mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Implement support for usernames
This commit is contained in:
parent
03f193b34c
commit
9f60ed534a
18 changed files with 440 additions and 46 deletions
|
@ -9,6 +9,7 @@ import org.asamk.signal.manager.api.Identity;
|
|||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||
import org.asamk.signal.manager.api.InvalidStickerException;
|
||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||
import org.asamk.signal.manager.api.Message;
|
||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||
|
@ -77,6 +78,18 @@ public interface Manager extends Closeable {
|
|||
*/
|
||||
void updateProfile(UpdateProfile updateProfile) throws IOException;
|
||||
|
||||
/**
|
||||
* Set a username for the account.
|
||||
* If the username is null, it will be deleted.
|
||||
*/
|
||||
String setUsername(String username) throws IOException, InvalidUsernameException;
|
||||
|
||||
/**
|
||||
* Set a username for the account.
|
||||
* If the username is null, it will be deleted.
|
||||
*/
|
||||
void deleteUsername() throws IOException;
|
||||
|
||||
void unregister() throws IOException;
|
||||
|
||||
void deleteAccount() throws IOException;
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.asamk.signal.manager.api.Identity;
|
|||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||
import org.asamk.signal.manager.api.InvalidStickerException;
|
||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||
import org.asamk.signal.manager.api.Message;
|
||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||
|
@ -65,6 +66,7 @@ import org.asamk.signal.manager.util.AttachmentUtils;
|
|||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.asamk.signal.manager.util.MimeUtils;
|
||||
import org.asamk.signal.manager.util.StickerUtils;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
@ -290,6 +292,20 @@ class ManagerImpl implements Manager {
|
|||
context.getSyncHelper().sendSyncFetchProfileMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String setUsername(final String username) throws IOException, InvalidUsernameException {
|
||||
try {
|
||||
return context.getAccountHelper().reserveUsername(username);
|
||||
} catch (BaseUsernameException e) {
|
||||
throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUsername() throws IOException {
|
||||
context.getAccountHelper().deleteUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister() throws IOException {
|
||||
context.getAccountHelper().unregister();
|
||||
|
@ -737,13 +753,18 @@ class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public void deleteRecipient(final RecipientIdentifier.Single recipient) {
|
||||
account.removeRecipient(account.getRecipientResolver().resolveRecipient(recipient.getIdentifier()));
|
||||
final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
|
||||
if (recipientIdOptional.isPresent()) {
|
||||
account.removeRecipient(recipientIdOptional.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteContact(final RecipientIdentifier.Single recipient) {
|
||||
account.getContactStore()
|
||||
.deleteContact(account.getRecipientResolver().resolveRecipient(recipient.getIdentifier()));
|
||||
final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
|
||||
if (recipientIdOptional.isPresent()) {
|
||||
account.getContactStore().deleteContact(recipientIdOptional.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package org.asamk.signal.manager.api;
|
||||
|
||||
public class InvalidUsernameException extends Exception {
|
||||
|
||||
public InvalidUsernameException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidUsernameException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public record RecipientAddress(Optional<UUID> uuid, Optional<String> number) {
|
||||
public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Optional<String> username) {
|
||||
|
||||
public static final UUID UNKNOWN_UUID = ServiceId.UNKNOWN.uuid();
|
||||
|
||||
|
@ -18,21 +18,25 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number) {
|
|||
*/
|
||||
public RecipientAddress {
|
||||
uuid = uuid.isPresent() && uuid.get().equals(UNKNOWN_UUID) ? Optional.empty() : uuid;
|
||||
if (uuid.isEmpty() && number.isEmpty()) {
|
||||
if (uuid.isEmpty() && number.isEmpty() && username.isEmpty()) {
|
||||
throw new AssertionError("Must have either a UUID or E164 number!");
|
||||
}
|
||||
}
|
||||
|
||||
public RecipientAddress(UUID uuid, String e164) {
|
||||
this(Optional.ofNullable(uuid), Optional.ofNullable(e164));
|
||||
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress(UUID uuid, String e164, String username) {
|
||||
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.ofNullable(username));
|
||||
}
|
||||
|
||||
public RecipientAddress(SignalServiceAddress address) {
|
||||
this(Optional.of(address.getServiceId().uuid()), address.getNumber());
|
||||
this(Optional.of(address.getServiceId().uuid()), address.getNumber(), Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress(UUID uuid) {
|
||||
this(Optional.of(uuid), Optional.empty());
|
||||
this(Optional.of(uuid), Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public ServiceId getServiceId() {
|
||||
|
@ -44,6 +48,8 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number) {
|
|||
return uuid.get().toString();
|
||||
} else if (number.isPresent()) {
|
||||
return number.get();
|
||||
} else if (username.isPresent()) {
|
||||
return username.get();
|
||||
} else {
|
||||
throw new AssertionError("Given the checks in the constructor, this should not be possible.");
|
||||
}
|
||||
|
@ -54,14 +60,16 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number) {
|
|||
return number.get();
|
||||
} else if (uuid.isPresent()) {
|
||||
return uuid.get().toString();
|
||||
} else if (username.isPresent()) {
|
||||
return username.get();
|
||||
} else {
|
||||
throw new AssertionError("Given the checks in the constructor, this should not be possible.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean matches(RecipientAddress other) {
|
||||
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) || (
|
||||
number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get())
|
||||
);
|
||||
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get()))
|
||||
|| (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get()))
|
||||
|| (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,10 @@ public sealed interface RecipientIdentifier {
|
|||
return new Uuid(UUID.fromString(identifier));
|
||||
}
|
||||
|
||||
if (identifier.startsWith("u:")) {
|
||||
return new Username(identifier.substring(2));
|
||||
}
|
||||
|
||||
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
||||
if (!normalizedNumber.equals(identifier)) {
|
||||
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
||||
|
@ -46,6 +50,8 @@ public sealed interface RecipientIdentifier {
|
|||
return new Number(address.number().get());
|
||||
} else if (address.uuid().isPresent()) {
|
||||
return new Uuid(address.uuid().get());
|
||||
} else if (address.username().isPresent()) {
|
||||
return new Username(address.username().get());
|
||||
}
|
||||
throw new AssertionError("RecipientAddress without identifier");
|
||||
}
|
||||
|
@ -79,6 +85,19 @@ public sealed interface RecipientIdentifier {
|
|||
}
|
||||
}
|
||||
|
||||
record Username(String username) implements Single {
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "u:" + username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientAddress toPartialRecipientAddress() {
|
||||
return new RecipientAddress(null, null, username);
|
||||
}
|
||||
}
|
||||
|
||||
record Group(GroupId groupId) implements RecipientIdentifier {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -15,6 +15,8 @@ import org.asamk.signal.manager.util.Utils;
|
|||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||
|
@ -27,13 +29,18 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE
|
|||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||
import org.whispersystems.util.Base64UrlSafe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||
|
||||
public class AccountHelper {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class);
|
||||
|
@ -173,6 +180,83 @@ public class AccountHelper {
|
|||
updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni()));
|
||||
}
|
||||
|
||||
public static final int USERNAME_MIN_LENGTH = 3;
|
||||
public static final int USERNAME_MAX_LENGTH = 32;
|
||||
|
||||
public String reserveUsername(String nickname) throws IOException, BaseUsernameException {
|
||||
final var currentUsername = account.getUsername();
|
||||
if (currentUsername != null) {
|
||||
final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
|
||||
if (currentNickname.equals(nickname)) {
|
||||
refreshCurrentUsername();
|
||||
return currentUsername;
|
||||
}
|
||||
}
|
||||
|
||||
final var candidates = Username.generateCandidates(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
|
||||
final var candidateHashes = new ArrayList<String>();
|
||||
for (final var candidate : candidates) {
|
||||
candidateHashes.add(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(candidate)));
|
||||
}
|
||||
|
||||
final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
|
||||
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
||||
if (hashIndex == -1) {
|
||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||
throw new IOException("Unexpected username response");
|
||||
}
|
||||
|
||||
logger.debug("[reserveUsername] Successfully reserved username.");
|
||||
final var username = candidates.get(hashIndex);
|
||||
|
||||
dependencies.getAccountManager().confirmUsername(username, response);
|
||||
account.setUsername(username);
|
||||
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
||||
logger.debug("[confirmUsername] Successfully confirmed username.");
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
public void refreshCurrentUsername() throws IOException, BaseUsernameException {
|
||||
final var localUsername = account.getUsername();
|
||||
if (localUsername == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
|
||||
final var serverUsernameHash = whoAmIResponse.getUsernameHash();
|
||||
final var hasServerUsername = !isEmpty(serverUsernameHash);
|
||||
final var localUsernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(localUsername));
|
||||
|
||||
if (!hasServerUsername) {
|
||||
logger.debug("No remote username is set.");
|
||||
}
|
||||
|
||||
if (!Objects.equals(localUsernameHash, serverUsernameHash)) {
|
||||
logger.debug("Local username hash does not match server username hash.");
|
||||
}
|
||||
|
||||
if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
|
||||
logger.debug("Attempting to resynchronize username.");
|
||||
tryReserveConfirmUsername(localUsername, localUsernameHash);
|
||||
} else {
|
||||
logger.debug("Username already set, not refreshing.");
|
||||
}
|
||||
}
|
||||
|
||||
private void tryReserveConfirmUsername(final String username, String localUsernameHash) throws IOException {
|
||||
final var response = dependencies.getAccountManager().reserveUsername(List.of(localUsernameHash));
|
||||
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
||||
dependencies.getAccountManager().confirmUsername(username, response);
|
||||
logger.debug("[confirmUsername] Successfully confirmed existing username.");
|
||||
}
|
||||
|
||||
public void deleteUsername() throws IOException {
|
||||
dependencies.getAccountManager().deleteUsername();
|
||||
account.setUsername(null);
|
||||
logger.debug("[deleteUsername] Successfully deleted the username.");
|
||||
}
|
||||
|
||||
public void setDeviceName(String deviceName) {
|
||||
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
||||
|
|
|
@ -6,6 +6,8 @@ import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
|||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
|
@ -13,6 +15,7 @@ import org.whispersystems.signalservice.api.push.PNI;
|
|||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.services.CdsiV2Service;
|
||||
import org.whispersystems.util.Base64UrlSafe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
@ -47,7 +50,7 @@ public class RecipientHelper {
|
|||
final var number = address.number().get();
|
||||
final ServiceId serviceId;
|
||||
try {
|
||||
serviceId = getRegisteredUser(number);
|
||||
serviceId = getRegisteredUserByNumber(number);
|
||||
} catch (UnregisteredRecipientException e) {
|
||||
logger.warn("Failed to get uuid for e164 number: {}", number);
|
||||
// Return SignalServiceAddress with unknown UUID
|
||||
|
@ -78,15 +81,33 @@ public class RecipientHelper {
|
|||
public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
|
||||
if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
|
||||
return account.getRecipientResolver().resolveRecipient(ServiceId.from(uuidRecipient.uuid()));
|
||||
} else {
|
||||
final var number = ((RecipientIdentifier.Number) recipient).number();
|
||||
return account.getRecipientStore().resolveRecipient(number, () -> {
|
||||
} else if (recipient instanceof RecipientIdentifier.Number numberRecipient) {
|
||||
final var number = numberRecipient.number();
|
||||
return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
|
||||
try {
|
||||
return getRegisteredUser(number);
|
||||
return getRegisteredUserByNumber(number);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
|
||||
final var username = usernameRecipient.username();
|
||||
return account.getRecipientStore().resolveRecipientByUsername(username, () -> {
|
||||
try {
|
||||
return getRegisteredUserByUsername(username);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
|
||||
}
|
||||
|
||||
public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
|
||||
try {
|
||||
return Optional.of(resolveRecipient(recipient));
|
||||
} catch (UnregisteredRecipientException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +117,7 @@ public class RecipientHelper {
|
|||
return recipientId;
|
||||
}
|
||||
final var number = address.getNumber().get();
|
||||
final var serviceId = getRegisteredUser(number);
|
||||
final var serviceId = getRegisteredUserByNumber(number);
|
||||
return account.getRecipientTrustedResolver()
|
||||
.resolveRecipientTrusted(new SignalServiceAddress(serviceId, number));
|
||||
}
|
||||
|
@ -111,7 +132,7 @@ public class RecipientHelper {
|
|||
return registeredUsers;
|
||||
}
|
||||
|
||||
private ServiceId getRegisteredUser(final String number) throws IOException, UnregisteredRecipientException {
|
||||
private ServiceId getRegisteredUserByNumber(final String number) throws IOException, UnregisteredRecipientException {
|
||||
final Map<String, RegisteredUser> aciMap;
|
||||
try {
|
||||
aciMap = getRegisteredUsers(Set.of(number));
|
||||
|
@ -153,8 +174,9 @@ public class RecipientHelper {
|
|||
return registeredUsers;
|
||||
}
|
||||
|
||||
private ACI getRegisteredUserByUsername(String username) throws IOException {
|
||||
return dependencies.getAccountManager().getAciByUsernameHash(username);
|
||||
private ACI getRegisteredUserByUsername(String username) throws IOException, BaseUsernameException {
|
||||
return dependencies.getAccountManager()
|
||||
.getAciByUsernameHash(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username)));
|
||||
}
|
||||
|
||||
public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
|
||||
|
|
|
@ -102,7 +102,11 @@ public class StorageHelper {
|
|||
|
||||
final var contactRecord = record.getContact().get();
|
||||
final var address = new RecipientAddress(contactRecord.getServiceId(), contactRecord.getNumber().orElse(null));
|
||||
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
||||
var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
||||
if (contactRecord.getUsername().isPresent()) {
|
||||
recipientId = account.getRecipientTrustedResolver()
|
||||
.resolveRecipientTrusted(contactRecord.getServiceId(), contactRecord.getUsername().get());
|
||||
}
|
||||
|
||||
final var contact = account.getContactStore().getContact(recipientId);
|
||||
final var blocked = contact != null && contact.isBlocked();
|
||||
|
@ -257,6 +261,7 @@ public class StorageHelper {
|
|||
});
|
||||
}
|
||||
account.getConfigurationStore().setPhoneNumberUnlisted(accountRecord.isPhoneNumberUnlisted());
|
||||
account.setUsername(accountRecord.getUsername());
|
||||
|
||||
if (accountRecord.getProfileKey().isPresent()) {
|
||||
ProfileKey profileKey;
|
||||
|
|
|
@ -22,7 +22,7 @@ import java.sql.SQLException;
|
|||
public class AccountDatabase extends Database {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||
private static final long DATABASE_VERSION = 11;
|
||||
private static final long DATABASE_VERSION = 12;
|
||||
|
||||
private AccountDatabase(final HikariDataSource dataSource) {
|
||||
super(logger, DATABASE_VERSION, dataSource);
|
||||
|
@ -296,5 +296,13 @@ public class AccountDatabase extends Database {
|
|||
""");
|
||||
}
|
||||
}
|
||||
if (oldVersion < 12) {
|
||||
logger.debug("Updating database: Adding username field");
|
||||
try (final var statement = connection.createStatement()) {
|
||||
statement.executeUpdate("""
|
||||
ALTER TABLE recipient ADD COLUMN username TEXT;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,6 +120,7 @@ public class SignalAccount implements Closeable {
|
|||
private String accountPath;
|
||||
private ServiceEnvironment serviceEnvironment;
|
||||
private String number;
|
||||
private String username;
|
||||
private ACI aci;
|
||||
private PNI pni;
|
||||
private String sessionId;
|
||||
|
@ -542,6 +543,9 @@ public class SignalAccount implements Closeable {
|
|||
serviceEnvironment = ServiceEnvironment.valueOf(rootNode.get("serviceEnvironment").asText());
|
||||
}
|
||||
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
|
||||
if (rootNode.hasNonNull("usernameIdentifier")) {
|
||||
username = rootNode.get("usernameIdentifier").asText();
|
||||
}
|
||||
if (rootNode.hasNonNull("uuid")) {
|
||||
try {
|
||||
aci = ACI.parseOrThrow(rootNode.get("uuid").asText());
|
||||
|
@ -935,6 +939,7 @@ public class SignalAccount implements Closeable {
|
|||
rootNode.put("version", CURRENT_STORAGE_VERSION)
|
||||
.put("username", number)
|
||||
.put("serviceEnvironment", serviceEnvironment == null ? null : serviceEnvironment.name())
|
||||
.put("usernameIdentifier", username)
|
||||
.put("uuid", aci == null ? null : aci.toString())
|
||||
.put("pni", pni == null ? null : pni.toString())
|
||||
.put("sessionId", sessionId)
|
||||
|
@ -1297,6 +1302,15 @@ public class SignalAccount implements Closeable {
|
|||
save();
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(final String username) {
|
||||
this.username = username;
|
||||
save();
|
||||
}
|
||||
|
||||
public ServiceEnvironment getServiceEnvironment() {
|
||||
return serviceEnvironment;
|
||||
}
|
||||
|
@ -1368,7 +1382,7 @@ public class SignalAccount implements Closeable {
|
|||
}
|
||||
|
||||
public RecipientAddress getSelfRecipientAddress() {
|
||||
return new RecipientAddress(aci, pni, number);
|
||||
return new RecipientAddress(aci, pni, number, username);
|
||||
}
|
||||
|
||||
public RecipientId getSelfRecipientId() {
|
||||
|
|
|
@ -6,7 +6,9 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|||
|
||||
import java.util.Optional;
|
||||
|
||||
public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni, Optional<String> number) {
|
||||
public record RecipientAddress(
|
||||
Optional<ServiceId> serviceId, Optional<PNI> pni, Optional<String> number, Optional<String> username
|
||||
) {
|
||||
|
||||
/**
|
||||
* Construct a RecipientAddress.
|
||||
|
@ -38,23 +40,30 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
}
|
||||
|
||||
public RecipientAddress(Optional<ServiceId> serviceId, Optional<String> number) {
|
||||
this(serviceId, Optional.empty(), number);
|
||||
this(serviceId, Optional.empty(), number, Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress(ServiceId serviceId, String e164) {
|
||||
this(Optional.ofNullable(serviceId), Optional.empty(), Optional.ofNullable(e164));
|
||||
this(Optional.ofNullable(serviceId), Optional.empty(), Optional.ofNullable(e164), Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress(ServiceId serviceId, PNI pni, String e164) {
|
||||
this(Optional.ofNullable(serviceId), Optional.ofNullable(pni), Optional.ofNullable(e164));
|
||||
this(Optional.ofNullable(serviceId), Optional.ofNullable(pni), Optional.ofNullable(e164), Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress(ServiceId serviceId, PNI pni, String e164, String username) {
|
||||
this(Optional.ofNullable(serviceId),
|
||||
Optional.ofNullable(pni),
|
||||
Optional.ofNullable(e164),
|
||||
Optional.ofNullable(username));
|
||||
}
|
||||
|
||||
public RecipientAddress(SignalServiceAddress address) {
|
||||
this(Optional.of(address.getServiceId()), Optional.empty(), address.getNumber());
|
||||
this(Optional.of(address.getServiceId()), Optional.empty(), address.getNumber(), Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress(org.asamk.signal.manager.api.RecipientAddress address) {
|
||||
this(address.uuid().map(ServiceId::from), Optional.empty(), address.number());
|
||||
this(address.uuid().map(ServiceId::from), Optional.empty(), address.number(), address.username());
|
||||
}
|
||||
|
||||
public RecipientAddress(ServiceId serviceId) {
|
||||
|
@ -66,7 +75,8 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
this.serviceId.isEmpty() || this.isServiceIdPNI() || this.serviceId.equals(address.pni)
|
||||
) && !address.isServiceIdPNI() ? address.serviceId : this.serviceId,
|
||||
address.pni.or(this::pni),
|
||||
address.number.or(this::number));
|
||||
address.number.or(this::number),
|
||||
address.username.or(this::username));
|
||||
}
|
||||
|
||||
public RecipientAddress removeIdentifiersFrom(RecipientAddress address) {
|
||||
|
@ -74,7 +84,8 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
? Optional.empty()
|
||||
: this.serviceId,
|
||||
address.pni.equals(this.pni) || address.serviceId.equals(this.pni) ? Optional.empty() : this.pni,
|
||||
address.number.equals(this.number) ? Optional.empty() : this.number);
|
||||
address.number.equals(this.number) ? Optional.empty() : this.number,
|
||||
address.username.equals(this.username) ? Optional.empty() : this.username);
|
||||
}
|
||||
|
||||
public ServiceId getServiceId() {
|
||||
|
@ -118,13 +129,17 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
}
|
||||
|
||||
public boolean hasSingleIdentifier() {
|
||||
return serviceId().isEmpty() || number.isEmpty();
|
||||
final var identifiersCount = serviceId().map(s -> 1).orElse(0)
|
||||
+ number().map(s -> 1).orElse(0)
|
||||
+ username().map(s -> 1).orElse(0);
|
||||
return identifiersCount == 1;
|
||||
}
|
||||
|
||||
public boolean hasIdentifiersOf(RecipientAddress address) {
|
||||
return (address.serviceId.isEmpty() || address.serviceId.equals(serviceId) || address.serviceId.equals(pni))
|
||||
&& (address.pni.isEmpty() || address.pni.equals(pni))
|
||||
&& (address.number.isEmpty() || address.number.equals(number));
|
||||
&& (address.number.isEmpty() || address.number.equals(number))
|
||||
&& (address.username.isEmpty() || address.username.equals(username));
|
||||
}
|
||||
|
||||
public boolean hasAdditionalIdentifiersThan(RecipientAddress address) {
|
||||
|
@ -142,6 +157,10 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
number.isPresent() && (
|
||||
address.number.isEmpty() || !address.number.equals(number)
|
||||
)
|
||||
) || (
|
||||
username.isPresent() && (
|
||||
address.username.isEmpty() || !address.username.equals(username)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -158,6 +177,8 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
}
|
||||
|
||||
public org.asamk.signal.manager.api.RecipientAddress toApiRecipientAddress() {
|
||||
return new org.asamk.signal.manager.api.RecipientAddress(serviceId().map(ServiceId::uuid), number());
|
||||
return new org.asamk.signal.manager.api.RecipientAddress(serviceId().map(ServiceId::uuid),
|
||||
number(),
|
||||
username());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
CREATE TABLE recipient (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
number TEXT UNIQUE,
|
||||
username TEXT UNIQUE,
|
||||
uuid BLOB UNIQUE,
|
||||
pni BLOB UNIQUE,
|
||||
profile_key BLOB,
|
||||
|
@ -93,7 +94,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT r.number, r.uuid, r.pni
|
||||
SELECT r.number, r.uuid, r.pni, r.username
|
||||
FROM %s r
|
||||
WHERE r._id = ?
|
||||
"""
|
||||
|
@ -193,7 +194,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
return new RecipientId(recipientId, this);
|
||||
}
|
||||
|
||||
public RecipientId resolveRecipient(
|
||||
public RecipientId resolveRecipientByNumber(
|
||||
final String number, Supplier<ServiceId> serviceIdSupplier
|
||||
) throws UnregisteredRecipientException {
|
||||
final Optional<RecipientWithAddress> byNumber;
|
||||
|
@ -214,6 +215,28 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
return byNumber.get().id();
|
||||
}
|
||||
|
||||
public RecipientId resolveRecipientByUsername(
|
||||
final String username, Supplier<ServiceId> serviceIdSupplier
|
||||
) throws UnregisteredRecipientException {
|
||||
final Optional<RecipientWithAddress> byUsername;
|
||||
try (final var connection = database.getConnection()) {
|
||||
byUsername = findByUsername(connection, username);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed read from recipient store", e);
|
||||
}
|
||||
if (byUsername.isEmpty() || byUsername.get().address().serviceId().isEmpty()) {
|
||||
final var serviceId = serviceIdSupplier.get();
|
||||
if (serviceId == null) {
|
||||
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
||||
null,
|
||||
username));
|
||||
}
|
||||
|
||||
return resolveRecipient(serviceId);
|
||||
}
|
||||
return byUsername.get().id();
|
||||
}
|
||||
|
||||
public RecipientId resolveRecipient(RecipientAddress address) {
|
||||
synchronized (recipientsLock) {
|
||||
final RecipientId recipientId;
|
||||
|
@ -247,7 +270,21 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
|
||||
) {
|
||||
final var serviceId = aci.map(a -> (ServiceId) a).or(() -> pni);
|
||||
return resolveRecipientTrusted(new RecipientAddress(serviceId, pni, number), false);
|
||||
return resolveRecipientTrusted(new RecipientAddress(serviceId, pni, number, Optional.empty()), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientId resolveRecipientTrusted(final ServiceId serviceId, final String username) {
|
||||
return resolveRecipientTrusted(new RecipientAddress(serviceId, null, null, username), false);
|
||||
}
|
||||
|
||||
public RecipientId resolveRecipientTrusted(
|
||||
final ACI aci, final String username
|
||||
) {
|
||||
return resolveRecipientTrusted(new RecipientAddress(Optional.of(aci),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(username)), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -309,7 +346,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final var sql = (
|
||||
"""
|
||||
SELECT r._id,
|
||||
r.number, r.uuid, r.pni,
|
||||
r.number, r.uuid, r.pni, r.username,
|
||||
r.profile_key, r.profile_key_credential,
|
||||
r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
|
||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities
|
||||
|
@ -739,7 +776,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET number = ?, uuid = ?, pni = ?
|
||||
SET number = ?, uuid = ?, pni = ?, username = ?
|
||||
WHERE _id = ?
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
|
@ -747,7 +784,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
statement.setString(1, address.number().orElse(null));
|
||||
statement.setBytes(2, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setBytes(3, address.pni().map(PNI::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setLong(4, recipientId.id());
|
||||
statement.setString(4, address.username().orElse(null));
|
||||
statement.setLong(5, recipientId.id());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
@ -800,7 +838,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final Connection connection, final String number
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.uuid, r.pni
|
||||
SELECT r._id, r.number, r.uuid, r.pni, r.username
|
||||
FROM %s r
|
||||
WHERE r.number = ?
|
||||
LIMIT 1
|
||||
|
@ -811,11 +849,26 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
}
|
||||
|
||||
private Optional<RecipientWithAddress> findByUsername(
|
||||
final Connection connection, final String username
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.uuid, r.pni, r.username
|
||||
FROM %s r
|
||||
WHERE r.username = ?
|
||||
LIMIT 1
|
||||
""".formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, username);
|
||||
return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<RecipientWithAddress> findByServiceId(
|
||||
final Connection connection, final ServiceId serviceId
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.uuid, r.pni
|
||||
SELECT r._id, r.number, r.uuid, r.pni, r.username
|
||||
FROM %s r
|
||||
WHERE r.uuid = ? OR r.pni = ?
|
||||
LIMIT 1
|
||||
|
@ -830,16 +883,18 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final Connection connection, final RecipientAddress address
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.uuid, r.pni
|
||||
SELECT r._id, r.number, r.uuid, r.pni, r.username
|
||||
FROM %s r
|
||||
WHERE r.uuid = ?1 OR r.pni = ?1 OR
|
||||
r.uuid = ?2 OR r.pni = ?2 OR
|
||||
r.number = ?3
|
||||
r.number = ?3 OR
|
||||
r.username = ?4
|
||||
""".formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setBytes(2, address.pni().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setString(3, address.number().orElse(null));
|
||||
statement.setString(4, address.username().orElse(null));
|
||||
return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
@ -908,7 +963,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final var serviceId = Optional.ofNullable(resultSet.getBytes("uuid")).map(ServiceId::parseOrNull);
|
||||
final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(PNI::parseOrNull);
|
||||
final var number = Optional.ofNullable(resultSet.getString("number"));
|
||||
return new RecipientAddress(serviceId, pni, number);
|
||||
final var username = Optional.ofNullable(resultSet.getString("username"));
|
||||
return new RecipientAddress(serviceId, pni, number, username);
|
||||
}
|
||||
|
||||
private RecipientId getRecipientIdFromResultSet(ResultSet resultSet) throws SQLException {
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.recipients;
|
|||
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Optional;
|
||||
|
@ -15,6 +16,8 @@ public interface RecipientTrustedResolver {
|
|||
|
||||
RecipientId resolveRecipientTrusted(Optional<ACI> aci, Optional<PNI> pni, Optional<String> number);
|
||||
|
||||
RecipientId resolveRecipientTrusted(ServiceId serviceId, String username);
|
||||
|
||||
class RecipientTrustedResolverWrapper implements RecipientTrustedResolver {
|
||||
|
||||
private final Supplier<RecipientTrustedResolver> recipientTrustedResolverSupplier;
|
||||
|
@ -39,5 +42,10 @@ public interface RecipientTrustedResolver {
|
|||
) {
|
||||
return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(aci, pni, number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientId resolveRecipientTrusted(final ServiceId serviceId, final String username) {
|
||||
return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(serviceId, username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue