]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
Implement support for usernames
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / AccountHelper.java
index 2b6c812bc431db5a5c78f8aeb8b5ec58c74e39cf..3bff549d0853ffd61a4df25186024aa1791b64ac 100644 (file)
@@ -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);