mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Implement support for usernames
This commit is contained in:
parent
03f193b34c
commit
9f60ed534a
18 changed files with 440 additions and 46 deletions
|
@ -195,6 +195,22 @@
|
||||||
{
|
{
|
||||||
"name":"org.signal.libsignal.protocol.state.SignedPreKeyStore"
|
"name":"org.signal.libsignal.protocol.state.SignedPreKeyStore"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.signal.libsignal.usernames.BadNicknameCharacterException",
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.signal.libsignal.usernames.CannotBeEmptyException",
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.signal.libsignal.usernames.NicknameTooLongException",
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.signal.libsignal.usernames.NicknameTooShortException",
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.signal.libsignal.zkgroup.InvalidInputException",
|
"name":"org.signal.libsignal.zkgroup.InvalidInputException",
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
|
|
@ -672,6 +672,13 @@
|
||||||
"queryAllDeclaredConstructors":true,
|
"queryAllDeclaredConstructors":true,
|
||||||
"methods":[{"name":"deviceLinkUri","parameterTypes":[] }]
|
"methods":[{"name":"deviceLinkUri","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.asamk.signal.commands.UpdateAccountCommand$JsonAccountResponse",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[{"name":"username","parameterTypes":[] }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.asamk.signal.commands.VerifyCommand$VerifyParams",
|
"name":"org.asamk.signal.commands.VerifyCommand$VerifyParams",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
@ -2535,6 +2542,12 @@
|
||||||
"allDeclaredMethods":true,
|
"allDeclaredMethods":true,
|
||||||
"allDeclaredConstructors":true
|
"allDeclaredConstructors":true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.whispersystems.signalservice.internal.push.ConfirmUsernameRequest",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.signalservice.internal.push.DeviceCode",
|
"name":"org.whispersystems.signalservice.internal.push.DeviceCode",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
@ -2553,6 +2566,13 @@
|
||||||
"allDeclaredMethods":true,
|
"allDeclaredMethods":true,
|
||||||
"allDeclaredConstructors":true
|
"allDeclaredConstructors":true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.whispersystems.signalservice.internal.push.GetAciByUsernameResponse",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.signalservice.internal.push.GroupMismatchedDevices",
|
"name":"org.whispersystems.signalservice.internal.push.GroupMismatchedDevices",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
@ -2699,6 +2719,19 @@
|
||||||
{"name":"getSkipDeviceTransfer","parameterTypes":[] }
|
{"name":"getSkipDeviceTransfer","parameterTypes":[] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.whispersystems.signalservice.internal.push.ReserveUsernameRequest",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.whispersystems.signalservice.internal.push.ReserveUsernameResponse",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.signalservice.internal.push.SendGroupMessageResponse",
|
"name":"org.whispersystems.signalservice.internal.push.SendGroupMessageResponse",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import org.asamk.signal.manager.api.Identity;
|
||||||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||||
import org.asamk.signal.manager.api.InvalidStickerException;
|
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.Message;
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||||
|
@ -77,6 +78,18 @@ public interface Manager extends Closeable {
|
||||||
*/
|
*/
|
||||||
void updateProfile(UpdateProfile updateProfile) throws IOException;
|
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 unregister() throws IOException;
|
||||||
|
|
||||||
void deleteAccount() 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.InactiveGroupLinkException;
|
||||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||||
import org.asamk.signal.manager.api.InvalidStickerException;
|
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.Message;
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
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.KeyUtils;
|
||||||
import org.asamk.signal.manager.util.MimeUtils;
|
import org.asamk.signal.manager.util.MimeUtils;
|
||||||
import org.asamk.signal.manager.util.StickerUtils;
|
import org.asamk.signal.manager.util.StickerUtils;
|
||||||
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||||
|
@ -290,6 +292,20 @@ class ManagerImpl implements Manager {
|
||||||
context.getSyncHelper().sendSyncFetchProfileMessage();
|
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
|
@Override
|
||||||
public void unregister() throws IOException {
|
public void unregister() throws IOException {
|
||||||
context.getAccountHelper().unregister();
|
context.getAccountHelper().unregister();
|
||||||
|
@ -737,13 +753,18 @@ class ManagerImpl implements Manager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteRecipient(final RecipientIdentifier.Single recipient) {
|
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
|
@Override
|
||||||
public void deleteContact(final RecipientIdentifier.Single recipient) {
|
public void deleteContact(final RecipientIdentifier.Single recipient) {
|
||||||
account.getContactStore()
|
final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
|
||||||
.deleteContact(account.getRecipientResolver().resolveRecipient(recipient.getIdentifier()));
|
if (recipientIdOptional.isPresent()) {
|
||||||
|
account.getContactStore().deleteContact(recipientIdOptional.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.Optional;
|
||||||
import java.util.UUID;
|
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();
|
public static final UUID UNKNOWN_UUID = ServiceId.UNKNOWN.uuid();
|
||||||
|
|
||||||
|
@ -18,21 +18,25 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number) {
|
||||||
*/
|
*/
|
||||||
public RecipientAddress {
|
public RecipientAddress {
|
||||||
uuid = uuid.isPresent() && uuid.get().equals(UNKNOWN_UUID) ? Optional.empty() : uuid;
|
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!");
|
throw new AssertionError("Must have either a UUID or E164 number!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientAddress(UUID uuid, String e164) {
|
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) {
|
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) {
|
public RecipientAddress(UUID uuid) {
|
||||||
this(Optional.of(uuid), Optional.empty());
|
this(Optional.of(uuid), Optional.empty(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceId getServiceId() {
|
public ServiceId getServiceId() {
|
||||||
|
@ -44,6 +48,8 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number) {
|
||||||
return uuid.get().toString();
|
return uuid.get().toString();
|
||||||
} else if (number.isPresent()) {
|
} else if (number.isPresent()) {
|
||||||
return number.get();
|
return number.get();
|
||||||
|
} else if (username.isPresent()) {
|
||||||
|
return username.get();
|
||||||
} else {
|
} else {
|
||||||
throw new AssertionError("Given the checks in the constructor, this should not be possible.");
|
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();
|
return number.get();
|
||||||
} else if (uuid.isPresent()) {
|
} else if (uuid.isPresent()) {
|
||||||
return uuid.get().toString();
|
return uuid.get().toString();
|
||||||
|
} else if (username.isPresent()) {
|
||||||
|
return username.get();
|
||||||
} else {
|
} else {
|
||||||
throw new AssertionError("Given the checks in the constructor, this should not be possible.");
|
throw new AssertionError("Given the checks in the constructor, this should not be possible.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(RecipientAddress other) {
|
public boolean matches(RecipientAddress other) {
|
||||||
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) || (
|
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get()))
|
||||||
number.isPresent() && other.number.isPresent() && number.get().equals(other.number.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));
|
return new Uuid(UUID.fromString(identifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (identifier.startsWith("u:")) {
|
||||||
|
return new Username(identifier.substring(2));
|
||||||
|
}
|
||||||
|
|
||||||
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
||||||
if (!normalizedNumber.equals(identifier)) {
|
if (!normalizedNumber.equals(identifier)) {
|
||||||
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
||||||
|
@ -46,6 +50,8 @@ public sealed interface RecipientIdentifier {
|
||||||
return new Number(address.number().get());
|
return new Number(address.number().get());
|
||||||
} else if (address.uuid().isPresent()) {
|
} else if (address.uuid().isPresent()) {
|
||||||
return new Uuid(address.uuid().get());
|
return new Uuid(address.uuid().get());
|
||||||
|
} else if (address.username().isPresent()) {
|
||||||
|
return new Username(address.username().get());
|
||||||
}
|
}
|
||||||
throw new AssertionError("RecipientAddress without identifier");
|
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 {
|
record Group(GroupId groupId) implements RecipientIdentifier {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -15,6 +15,8 @@ import org.asamk.signal.manager.util.Utils;
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
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.push.exceptions.DeprecatedVersionException;
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||||
|
import org.whispersystems.util.Base64UrlSafe;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||||
|
|
||||||
public class AccountHelper {
|
public class AccountHelper {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class);
|
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()));
|
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) {
|
public void setDeviceName(String deviceName) {
|
||||||
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
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.config.ServiceEnvironmentConfig;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.ACI;
|
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.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.services.CdsiV2Service;
|
import org.whispersystems.signalservice.api.services.CdsiV2Service;
|
||||||
|
import org.whispersystems.util.Base64UrlSafe;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -47,7 +50,7 @@ public class RecipientHelper {
|
||||||
final var number = address.number().get();
|
final var number = address.number().get();
|
||||||
final ServiceId serviceId;
|
final ServiceId serviceId;
|
||||||
try {
|
try {
|
||||||
serviceId = getRegisteredUser(number);
|
serviceId = getRegisteredUserByNumber(number);
|
||||||
} catch (UnregisteredRecipientException e) {
|
} catch (UnregisteredRecipientException e) {
|
||||||
logger.warn("Failed to get uuid for e164 number: {}", number);
|
logger.warn("Failed to get uuid for e164 number: {}", number);
|
||||||
// Return SignalServiceAddress with unknown UUID
|
// Return SignalServiceAddress with unknown UUID
|
||||||
|
@ -78,15 +81,33 @@ public class RecipientHelper {
|
||||||
public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
|
public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
|
||||||
if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
|
if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
|
||||||
return account.getRecipientResolver().resolveRecipient(ServiceId.from(uuidRecipient.uuid()));
|
return account.getRecipientResolver().resolveRecipient(ServiceId.from(uuidRecipient.uuid()));
|
||||||
} else {
|
} else if (recipient instanceof RecipientIdentifier.Number numberRecipient) {
|
||||||
final var number = ((RecipientIdentifier.Number) recipient).number();
|
final var number = numberRecipient.number();
|
||||||
return account.getRecipientStore().resolveRecipient(number, () -> {
|
return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
|
||||||
try {
|
try {
|
||||||
return getRegisteredUser(number);
|
return getRegisteredUserByNumber(number);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
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;
|
return recipientId;
|
||||||
}
|
}
|
||||||
final var number = address.getNumber().get();
|
final var number = address.getNumber().get();
|
||||||
final var serviceId = getRegisteredUser(number);
|
final var serviceId = getRegisteredUserByNumber(number);
|
||||||
return account.getRecipientTrustedResolver()
|
return account.getRecipientTrustedResolver()
|
||||||
.resolveRecipientTrusted(new SignalServiceAddress(serviceId, number));
|
.resolveRecipientTrusted(new SignalServiceAddress(serviceId, number));
|
||||||
}
|
}
|
||||||
|
@ -111,7 +132,7 @@ public class RecipientHelper {
|
||||||
return registeredUsers;
|
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;
|
final Map<String, RegisteredUser> aciMap;
|
||||||
try {
|
try {
|
||||||
aciMap = getRegisteredUsers(Set.of(number));
|
aciMap = getRegisteredUsers(Set.of(number));
|
||||||
|
@ -153,8 +174,9 @@ public class RecipientHelper {
|
||||||
return registeredUsers;
|
return registeredUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ACI getRegisteredUserByUsername(String username) throws IOException {
|
private ACI getRegisteredUserByUsername(String username) throws IOException, BaseUsernameException {
|
||||||
return dependencies.getAccountManager().getAciByUsernameHash(username);
|
return dependencies.getAccountManager()
|
||||||
|
.getAciByUsernameHash(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
|
public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
|
||||||
|
|
|
@ -102,7 +102,11 @@ public class StorageHelper {
|
||||||
|
|
||||||
final var contactRecord = record.getContact().get();
|
final var contactRecord = record.getContact().get();
|
||||||
final var address = new RecipientAddress(contactRecord.getServiceId(), contactRecord.getNumber().orElse(null));
|
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 contact = account.getContactStore().getContact(recipientId);
|
||||||
final var blocked = contact != null && contact.isBlocked();
|
final var blocked = contact != null && contact.isBlocked();
|
||||||
|
@ -257,6 +261,7 @@ public class StorageHelper {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
account.getConfigurationStore().setPhoneNumberUnlisted(accountRecord.isPhoneNumberUnlisted());
|
account.getConfigurationStore().setPhoneNumberUnlisted(accountRecord.isPhoneNumberUnlisted());
|
||||||
|
account.setUsername(accountRecord.getUsername());
|
||||||
|
|
||||||
if (accountRecord.getProfileKey().isPresent()) {
|
if (accountRecord.getProfileKey().isPresent()) {
|
||||||
ProfileKey profileKey;
|
ProfileKey profileKey;
|
||||||
|
|
|
@ -22,7 +22,7 @@ import java.sql.SQLException;
|
||||||
public class AccountDatabase extends Database {
|
public class AccountDatabase extends Database {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
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) {
|
private AccountDatabase(final HikariDataSource dataSource) {
|
||||||
super(logger, DATABASE_VERSION, 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 String accountPath;
|
||||||
private ServiceEnvironment serviceEnvironment;
|
private ServiceEnvironment serviceEnvironment;
|
||||||
private String number;
|
private String number;
|
||||||
|
private String username;
|
||||||
private ACI aci;
|
private ACI aci;
|
||||||
private PNI pni;
|
private PNI pni;
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
@ -542,6 +543,9 @@ public class SignalAccount implements Closeable {
|
||||||
serviceEnvironment = ServiceEnvironment.valueOf(rootNode.get("serviceEnvironment").asText());
|
serviceEnvironment = ServiceEnvironment.valueOf(rootNode.get("serviceEnvironment").asText());
|
||||||
}
|
}
|
||||||
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
|
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
|
||||||
|
if (rootNode.hasNonNull("usernameIdentifier")) {
|
||||||
|
username = rootNode.get("usernameIdentifier").asText();
|
||||||
|
}
|
||||||
if (rootNode.hasNonNull("uuid")) {
|
if (rootNode.hasNonNull("uuid")) {
|
||||||
try {
|
try {
|
||||||
aci = ACI.parseOrThrow(rootNode.get("uuid").asText());
|
aci = ACI.parseOrThrow(rootNode.get("uuid").asText());
|
||||||
|
@ -935,6 +939,7 @@ public class SignalAccount implements Closeable {
|
||||||
rootNode.put("version", CURRENT_STORAGE_VERSION)
|
rootNode.put("version", CURRENT_STORAGE_VERSION)
|
||||||
.put("username", number)
|
.put("username", number)
|
||||||
.put("serviceEnvironment", serviceEnvironment == null ? null : serviceEnvironment.name())
|
.put("serviceEnvironment", serviceEnvironment == null ? null : serviceEnvironment.name())
|
||||||
|
.put("usernameIdentifier", username)
|
||||||
.put("uuid", aci == null ? null : aci.toString())
|
.put("uuid", aci == null ? null : aci.toString())
|
||||||
.put("pni", pni == null ? null : pni.toString())
|
.put("pni", pni == null ? null : pni.toString())
|
||||||
.put("sessionId", sessionId)
|
.put("sessionId", sessionId)
|
||||||
|
@ -1297,6 +1302,15 @@ public class SignalAccount implements Closeable {
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(final String username) {
|
||||||
|
this.username = username;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
public ServiceEnvironment getServiceEnvironment() {
|
public ServiceEnvironment getServiceEnvironment() {
|
||||||
return serviceEnvironment;
|
return serviceEnvironment;
|
||||||
}
|
}
|
||||||
|
@ -1368,7 +1382,7 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientAddress getSelfRecipientAddress() {
|
public RecipientAddress getSelfRecipientAddress() {
|
||||||
return new RecipientAddress(aci, pni, number);
|
return new RecipientAddress(aci, pni, number, username);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientId getSelfRecipientId() {
|
public RecipientId getSelfRecipientId() {
|
||||||
|
|
|
@ -6,7 +6,9 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
import java.util.Optional;
|
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.
|
* Construct a RecipientAddress.
|
||||||
|
@ -38,23 +40,30 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientAddress(Optional<ServiceId> serviceId, Optional<String> number) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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)
|
this.serviceId.isEmpty() || this.isServiceIdPNI() || this.serviceId.equals(address.pni)
|
||||||
) && !address.isServiceIdPNI() ? address.serviceId : this.serviceId,
|
) && !address.isServiceIdPNI() ? address.serviceId : this.serviceId,
|
||||||
address.pni.or(this::pni),
|
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) {
|
public RecipientAddress removeIdentifiersFrom(RecipientAddress address) {
|
||||||
|
@ -74,7 +84,8 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
||||||
? Optional.empty()
|
? Optional.empty()
|
||||||
: this.serviceId,
|
: this.serviceId,
|
||||||
address.pni.equals(this.pni) || address.serviceId.equals(this.pni) ? Optional.empty() : this.pni,
|
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() {
|
public ServiceId getServiceId() {
|
||||||
|
@ -118,13 +129,17 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasSingleIdentifier() {
|
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) {
|
public boolean hasIdentifiersOf(RecipientAddress address) {
|
||||||
return (address.serviceId.isEmpty() || address.serviceId.equals(serviceId) || address.serviceId.equals(pni))
|
return (address.serviceId.isEmpty() || address.serviceId.equals(serviceId) || address.serviceId.equals(pni))
|
||||||
&& (address.pni.isEmpty() || address.pni.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) {
|
public boolean hasAdditionalIdentifiersThan(RecipientAddress address) {
|
||||||
|
@ -142,6 +157,10 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
||||||
number.isPresent() && (
|
number.isPresent() && (
|
||||||
address.number.isEmpty() || !address.number.equals(number)
|
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() {
|
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 (
|
CREATE TABLE recipient (
|
||||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
number TEXT UNIQUE,
|
number TEXT UNIQUE,
|
||||||
|
username TEXT UNIQUE,
|
||||||
uuid BLOB UNIQUE,
|
uuid BLOB UNIQUE,
|
||||||
pni BLOB UNIQUE,
|
pni BLOB UNIQUE,
|
||||||
profile_key BLOB,
|
profile_key BLOB,
|
||||||
|
@ -93,7 +94,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||||
public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
|
public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT r.number, r.uuid, r.pni
|
SELECT r.number, r.uuid, r.pni, r.username
|
||||||
FROM %s r
|
FROM %s r
|
||||||
WHERE r._id = ?
|
WHERE r._id = ?
|
||||||
"""
|
"""
|
||||||
|
@ -193,7 +194,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||||
return new RecipientId(recipientId, this);
|
return new RecipientId(recipientId, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientId resolveRecipient(
|
public RecipientId resolveRecipientByNumber(
|
||||||
final String number, Supplier<ServiceId> serviceIdSupplier
|
final String number, Supplier<ServiceId> serviceIdSupplier
|
||||||
) throws UnregisteredRecipientException {
|
) throws UnregisteredRecipientException {
|
||||||
final Optional<RecipientWithAddress> byNumber;
|
final Optional<RecipientWithAddress> byNumber;
|
||||||
|
@ -214,6 +215,28 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||||
return byNumber.get().id();
|
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) {
|
public RecipientId resolveRecipient(RecipientAddress address) {
|
||||||
synchronized (recipientsLock) {
|
synchronized (recipientsLock) {
|
||||||
final RecipientId recipientId;
|
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 Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
|
||||||
) {
|
) {
|
||||||
final var serviceId = aci.map(a -> (ServiceId) a).or(() -> pni);
|
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
|
@Override
|
||||||
|
@ -309,7 +346,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT r._id,
|
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.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.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
|
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 = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET number = ?, uuid = ?, pni = ?
|
SET number = ?, uuid = ?, pni = ?, username = ?
|
||||||
WHERE _id = ?
|
WHERE _id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
|
@ -747,7 +784,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||||
statement.setString(1, address.number().orElse(null));
|
statement.setString(1, address.number().orElse(null));
|
||||||
statement.setBytes(2, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).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.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();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -800,7 +838,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||||
final Connection connection, final String number
|
final Connection connection, final String number
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var sql = """
|
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
|
FROM %s r
|
||||||
WHERE r.number = ?
|
WHERE r.number = ?
|
||||||
LIMIT 1
|
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(
|
private Optional<RecipientWithAddress> findByServiceId(
|
||||||
final Connection connection, final ServiceId serviceId
|
final Connection connection, final ServiceId serviceId
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var sql = """
|
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
|
FROM %s r
|
||||||
WHERE r.uuid = ? OR r.pni = ?
|
WHERE r.uuid = ? OR r.pni = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
|
@ -830,16 +883,18 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||||
final Connection connection, final RecipientAddress address
|
final Connection connection, final RecipientAddress address
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var sql = """
|
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
|
FROM %s r
|
||||||
WHERE r.uuid = ?1 OR r.pni = ?1 OR
|
WHERE r.uuid = ?1 OR r.pni = ?1 OR
|
||||||
r.uuid = ?2 OR r.pni = ?2 OR
|
r.uuid = ?2 OR r.pni = ?2 OR
|
||||||
r.number = ?3
|
r.number = ?3 OR
|
||||||
|
r.username = ?4
|
||||||
""".formatted(TABLE_RECIPIENT);
|
""".formatted(TABLE_RECIPIENT);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setBytes(1, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
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.setBytes(2, address.pni().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||||
statement.setString(3, address.number().orElse(null));
|
statement.setString(3, address.number().orElse(null));
|
||||||
|
statement.setString(4, address.username().orElse(null));
|
||||||
return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet)
|
return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet)
|
||||||
.collect(Collectors.toSet());
|
.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 serviceId = Optional.ofNullable(resultSet.getBytes("uuid")).map(ServiceId::parseOrNull);
|
||||||
final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(PNI::parseOrNull);
|
final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(PNI::parseOrNull);
|
||||||
final var number = Optional.ofNullable(resultSet.getString("number"));
|
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 {
|
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.ACI;
|
||||||
import org.whispersystems.signalservice.api.push.PNI;
|
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.push.SignalServiceAddress;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -15,6 +16,8 @@ public interface RecipientTrustedResolver {
|
||||||
|
|
||||||
RecipientId resolveRecipientTrusted(Optional<ACI> aci, Optional<PNI> pni, Optional<String> number);
|
RecipientId resolveRecipientTrusted(Optional<ACI> aci, Optional<PNI> pni, Optional<String> number);
|
||||||
|
|
||||||
|
RecipientId resolveRecipientTrusted(ServiceId serviceId, String username);
|
||||||
|
|
||||||
class RecipientTrustedResolverWrapper implements RecipientTrustedResolver {
|
class RecipientTrustedResolverWrapper implements RecipientTrustedResolver {
|
||||||
|
|
||||||
private final Supplier<RecipientTrustedResolver> recipientTrustedResolverSupplier;
|
private final Supplier<RecipientTrustedResolver> recipientTrustedResolverSupplier;
|
||||||
|
@ -39,5 +42,10 @@ public interface RecipientTrustedResolver {
|
||||||
) {
|
) {
|
||||||
return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(aci, pni, number);
|
return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(aci, pni, number);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveRecipientTrusted(final ServiceId serviceId, final String username) {
|
||||||
|
return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(serviceId, username);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,12 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
|
||||||
for (var r : recipients) {
|
for (var r : recipients) {
|
||||||
final var contact = r.getContact() == null ? Contact.newBuilder().build() : r.getContact();
|
final var contact = r.getContact() == null ? Contact.newBuilder().build() : r.getContact();
|
||||||
final var profile = r.getProfile() == null ? Profile.newBuilder().build() : r.getProfile();
|
final var profile = r.getProfile() == null ? Profile.newBuilder().build() : r.getProfile();
|
||||||
writer.println("Number: {} Name: {} Profile name: {} Color: {} Blocked: {} Message expiration: {}",
|
writer.println(
|
||||||
|
"Number: {} Name: {} Profile name: {} Username: {} Color: {} Blocked: {} Message expiration: {}",
|
||||||
r.getAddress().getLegacyIdentifier(),
|
r.getAddress().getLegacyIdentifier(),
|
||||||
contact.getName(),
|
contact.getName(),
|
||||||
profile.getDisplayName(),
|
profile.getDisplayName(),
|
||||||
|
r.getAddress().username().orElse(""),
|
||||||
contact.getColor(),
|
contact.getColor(),
|
||||||
contact.isBlocked(),
|
contact.isBlocked(),
|
||||||
contact.getMessageExpirationTime() == 0
|
contact.getMessageExpirationTime() == 0
|
||||||
|
@ -72,6 +74,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
|
||||||
final var contact = r.getContact() == null ? Contact.newBuilder().build() : r.getContact();
|
final var contact = r.getContact() == null ? Contact.newBuilder().build() : r.getContact();
|
||||||
return new JsonContact(address.number().orElse(null),
|
return new JsonContact(address.number().orElse(null),
|
||||||
address.uuid().map(UUID::toString).orElse(null),
|
address.uuid().map(UUID::toString).orElse(null),
|
||||||
|
address.username().orElse(null),
|
||||||
contact.getName(),
|
contact.getName(),
|
||||||
contact.getColor(),
|
contact.getColor(),
|
||||||
contact.isBlocked(),
|
contact.isBlocked(),
|
||||||
|
@ -96,6 +99,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
|
||||||
private record JsonContact(
|
private record JsonContact(
|
||||||
String number,
|
String number,
|
||||||
String uuid,
|
String uuid,
|
||||||
|
String username,
|
||||||
String name,
|
String name,
|
||||||
String color,
|
String color,
|
||||||
boolean isBlocked,
|
boolean isBlocked,
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
package org.asamk.signal.commands;
|
package org.asamk.signal.commands;
|
||||||
|
|
||||||
|
import net.sourceforge.argparse4j.impl.Arguments;
|
||||||
import net.sourceforge.argparse4j.inf.Namespace;
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
import net.sourceforge.argparse4j.inf.Subparser;
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
|
|
||||||
import org.asamk.signal.commands.exceptions.CommandException;
|
import org.asamk.signal.commands.exceptions.CommandException;
|
||||||
import org.asamk.signal.commands.exceptions.IOErrorException;
|
import org.asamk.signal.commands.exceptions.IOErrorException;
|
||||||
|
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
|
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||||
|
import org.asamk.signal.output.JsonWriter;
|
||||||
import org.asamk.signal.output.OutputWriter;
|
import org.asamk.signal.output.OutputWriter;
|
||||||
|
import org.asamk.signal.output.PlainTextWriter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@ -21,6 +26,11 @@ public class UpdateAccountCommand implements JsonRpcLocalCommand {
|
||||||
public void attachToSubparser(final Subparser subparser) {
|
public void attachToSubparser(final Subparser subparser) {
|
||||||
subparser.help("Update the account attributes on the signal server.");
|
subparser.help("Update the account attributes on the signal server.");
|
||||||
subparser.addArgument("-n", "--device-name").help("Specify a name to describe this device.");
|
subparser.addArgument("-n", "--device-name").help("Specify a name to describe this device.");
|
||||||
|
var mut = subparser.addMutuallyExclusiveGroup();
|
||||||
|
mut.addArgument("-u", "--username").help("Specify a username that can then be used to contact this account.");
|
||||||
|
mut.addArgument("--delete-username")
|
||||||
|
.action(Arguments.storeTrue())
|
||||||
|
.help("Delete the username associated with this account.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -33,5 +43,34 @@ public class UpdateAccountCommand implements JsonRpcLocalCommand {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IOErrorException("UpdateAccount error: " + e.getMessage(), e);
|
throw new IOErrorException("UpdateAccount error: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var username = ns.getString("username");
|
||||||
|
if (username != null) {
|
||||||
|
try {
|
||||||
|
final var newUsername = m.setUsername(username);
|
||||||
|
if (outputWriter instanceof PlainTextWriter w) {
|
||||||
|
w.println("Your new username: {}", newUsername);
|
||||||
|
} else if (outputWriter instanceof JsonWriter w) {
|
||||||
|
w.write(new JsonAccountResponse(newUsername));
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Failed to set username: " + e.getMessage(), e);
|
||||||
|
} catch (InvalidUsernameException e) {
|
||||||
|
throw new UserErrorException("Invalid username: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleteUsername = Boolean.TRUE.equals(ns.getBoolean("delete-username"));
|
||||||
|
if (deleteUsername) {
|
||||||
|
try {
|
||||||
|
m.deleteUsername();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Failed to delete username: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record JsonAccountResponse(
|
||||||
|
String username
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.asamk.signal.manager.api.Group;
|
||||||
import org.asamk.signal.manager.api.Identity;
|
import org.asamk.signal.manager.api.Identity;
|
||||||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||||
|
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||||
import org.asamk.signal.manager.api.Message;
|
import org.asamk.signal.manager.api.Message;
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||||
|
@ -151,6 +152,16 @@ public class DbusManagerImpl implements Manager {
|
||||||
updateProfile.isDeleteAvatar());
|
updateProfile.isDeleteAvatar());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String setUsername(final String username) throws IOException, InvalidUsernameException {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteUsername() throws IOException {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unregister() throws IOException {
|
public void unregister() throws IOException {
|
||||||
signal.unregister();
|
signal.unregister();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue