]> nmode's Git Repositories - signal-cli/commitdiff
Implement username links
authorAsamK <asamk@gmx.de>
Thu, 16 Nov 2023 19:18:15 +0000 (20:18 +0100)
committerAsamK <asamk@gmx.de>
Tue, 21 Nov 2023 16:09:21 +0000 (17:09 +0100)
graalvm-config-dir/jni-config.json
graalvm-config-dir/reflect-config.json
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/api/UsernameLinkUrl.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java
src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java

index a7e630385e6f6721bdb9f9af21151934ada674fb..5b47a7b6debfcb33d882e6db9c3ce05070933d88 100644 (file)
   "name":"org.signal.libsignal.usernames.CannotBeEmptyException",
   "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.MissingSeparatorException",
+  "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.NicknameTooLongException",
   "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
index 9f2935ce79be63b529094bc96c3b2b92301b8ac0..7bfa2e71a0f4dc2a646723dadc17178633ac5c10 100644 (file)
   "allDeclaredFields":true,
   "queryAllDeclaredMethods":true,
   "queryAllDeclaredConstructors":true,
   "allDeclaredFields":true,
   "queryAllDeclaredMethods":true,
   "queryAllDeclaredConstructors":true,
-  "methods":[{"name":"username","parameterTypes":[] }]
+  "methods":[{"name":"username","parameterTypes":[] }, {"name":"usernameLink","parameterTypes":[] }]
 },
 {
   "name":"org.asamk.signal.commands.VerifyCommand$VerifyParams",
 },
 {
   "name":"org.asamk.signal.commands.VerifyCommand$VerifyParams",
   "name":"org.whispersystems.signalservice.internal.push.SenderCertificate$ByteArrayDesieralizer",
   "methods":[{"name":"<init>","parameterTypes":[] }]
 },
   "name":"org.whispersystems.signalservice.internal.push.SenderCertificate$ByteArrayDesieralizer",
   "methods":[{"name":"<init>","parameterTypes":[] }]
 },
+{
+  "name":"org.whispersystems.signalservice.internal.push.SetUsernameLinkRequestBody",
+  "allDeclaredFields":true,
+  "queryAllDeclaredMethods":true,
+  "queryAllDeclaredConstructors":true,
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }, {"name":"getUsernameLinkEncryptedValue","parameterTypes":[] }]
+},
+{
+  "name":"org.whispersystems.signalservice.internal.push.SetUsernameLinkResponseBody",
+  "allDeclaredFields":true,
+  "queryAllDeclaredMethods":true,
+  "queryAllDeclaredConstructors":true,
+  "methods":[{"name":"<init>","parameterTypes":["java.util.UUID"] }, {"name":"<init>","parameterTypes":["java.util.UUID","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
+},
 {
   "name":"org.whispersystems.signalservice.internal.push.StaleDevices",
   "allDeclaredFields":true,
 {
   "name":"org.whispersystems.signalservice.internal.push.StaleDevices",
   "allDeclaredFields":true,
index 6153000b8e0e0fa7e477c34ef6ff3a5038f13263..ac88e2bca040696191b056a4e73e5f5eacbb836e 100644 (file)
@@ -41,6 +41,7 @@ import org.asamk.signal.manager.api.UnregisteredRecipientException;
 import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
 import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
+import org.asamk.signal.manager.api.UsernameLinkUrl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
@@ -100,11 +101,15 @@ public interface Manager extends Closeable {
      */
     void updateProfile(UpdateProfile updateProfile) throws IOException;
 
      */
     void updateProfile(UpdateProfile updateProfile) throws IOException;
 
+    String getUsername();
+
+    UsernameLinkUrl getUsernameLink();
+
     /**
      * Set a username for the account.
      * If the username is null, it will be deleted.
      */
     /**
      * Set a username for the account.
      * If the username is null, it will be deleted.
      */
-    String setUsername(String username) throws IOException, InvalidUsernameException;
+    void setUsername(String username) throws IOException, InvalidUsernameException;
 
     /**
      * Set a username for the account.
 
     /**
      * Set a username for the account.
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UsernameLinkUrl.java b/lib/src/main/java/org/asamk/signal/manager/api/UsernameLinkUrl.java
new file mode 100644 (file)
index 0000000..f8fff6e
--- /dev/null
@@ -0,0 +1,82 @@
+package org.asamk.signal.manager.api;
+
+import org.signal.core.util.Base64;
+import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+public final class UsernameLinkUrl {
+
+    private static final Pattern URL_REGEX = Pattern.compile("(https://)?signal.me/?#eu/([a-zA-Z0-9+\\-_/]+)");
+
+    private static final String BASE_URL = "https://signal.me/#eu/";
+
+    private final String url;
+    private final UsernameLinkComponents usernameLinkComponents;
+
+    public static UsernameLinkUrl fromUri(String url) throws InvalidUsernameLinkException {
+        final var matcher = URL_REGEX.matcher(url);
+        if (!matcher.matches()) {
+            throw new InvalidUsernameLinkException("Invalid username link");
+        }
+        final var path = matcher.group(2);
+        final byte[] allBytes;
+        try {
+            allBytes = Base64.decode(path);
+        } catch (IOException e) {
+            throw new InvalidUsernameLinkException("Invalid base64 encoding");
+        }
+
+        if (allBytes.length != 48) {
+            throw new InvalidUsernameLinkException("Invalid username link");
+        }
+
+        final var entropy = Arrays.copyOfRange(allBytes, 0, 32);
+        final var serverId = Arrays.copyOfRange(allBytes, 32, allBytes.length);
+        final var serverIdUuid = UuidUtil.parseOrNull(serverId);
+        if (serverIdUuid == null) {
+            throw new InvalidUsernameLinkException("Invalid serverId");
+        }
+
+        return new UsernameLinkUrl(new UsernameLinkComponents(entropy, serverIdUuid));
+    }
+
+    public UsernameLinkUrl(UsernameLinkComponents usernameLinkComponents) {
+        this.usernameLinkComponents = usernameLinkComponents;
+        this.url = createUrl(usernameLinkComponents);
+    }
+
+    private static String createUrl(UsernameLinkComponents usernameLinkComponents) {
+        final var entropy = usernameLinkComponents.getEntropy();
+        final var serverId = UuidUtil.toByteArray(usernameLinkComponents.getServerId());
+
+        final var combined = new byte[entropy.length + serverId.length];
+        System.arraycopy(entropy, 0, combined, 0, entropy.length);
+        System.arraycopy(serverId, 0, combined, entropy.length, serverId.length);
+
+        final var base64 = Base64.encodeUrlSafeWithoutPadding(combined);
+        return BASE_URL + base64;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public UsernameLinkComponents getComponents() {
+        return usernameLinkComponents;
+    }
+
+    public static final class InvalidUsernameLinkException extends Exception {
+
+        public InvalidUsernameLinkException(String message) {
+            super(message);
+        }
+
+        public InvalidUsernameLinkException(Throwable cause) {
+            super(cause);
+        }
+    }
+}
index d56b2379463d44a4a6ccc01405c503cee121f523..32642f2851579991641072feb1776b72ab4ec89f 100644 (file)
@@ -33,6 +33,9 @@ import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
 import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
 import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
 import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
 import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
+import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
+import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
+import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
 import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
 import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
 import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
 import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
@@ -98,6 +101,13 @@ public class AccountHelper {
                     && account.getRegistrationLockPin() != null) {
                 migrateRegistrationPin();
             }
                     && account.getRegistrationLockPin() != null) {
                 migrateRegistrationPin();
             }
+            if (account.getUsername() != null && account.getUsernameLink() == null) {
+                try {
+                    tryToSetUsernameLink(new Username(account.getUsername()));
+                } catch (BaseUsernameException e) {
+                    logger.debug("Invalid local username");
+                }
+            }
         } catch (DeprecatedVersionException e) {
             logger.debug("Signal-Server returned deprecated version exception", e);
             throw e;
         } catch (DeprecatedVersionException e) {
             logger.debug("Signal-Server returned deprecated version exception", e);
             throw e;
@@ -305,13 +315,13 @@ public class AccountHelper {
     public static final int USERNAME_MIN_LENGTH = 3;
     public static final int USERNAME_MAX_LENGTH = 32;
 
     public static final int USERNAME_MIN_LENGTH = 3;
     public static final int USERNAME_MAX_LENGTH = 32;
 
-    public String reserveUsername(String nickname) throws IOException, BaseUsernameException {
+    public void 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();
         final var currentUsername = account.getUsername();
         if (currentUsername != null) {
             final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
             if (currentNickname.equals(nickname)) {
                 refreshCurrentUsername();
-                return currentUsername;
+                return;
             }
         }
 
             }
         }
 
@@ -329,14 +339,13 @@ public class AccountHelper {
         }
 
         logger.debug("[reserveUsername] Successfully reserved username.");
         }
 
         logger.debug("[reserveUsername] Successfully reserved username.");
-        final var username = candidates.get(hashIndex).getUsername();
+        final var username = candidates.get(hashIndex);
 
 
-        dependencies.getAccountManager().confirmUsername(username, response);
-        account.setUsername(username);
+        dependencies.getAccountManager().confirmUsername(username.getUsername(), response);
+        account.setUsername(username.getUsername());
         account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
         logger.debug("[confirmUsername] Successfully confirmed username.");
         account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
         logger.debug("[confirmUsername] Successfully confirmed username.");
-
-        return username;
+        tryToSetUsernameLink(username);
     }
 
     public void refreshCurrentUsername() throws IOException, BaseUsernameException {
     }
 
     public void refreshCurrentUsername() throws IOException, BaseUsernameException {
@@ -348,7 +357,8 @@ public class AccountHelper {
         final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
         final var serverUsernameHash = whoAmIResponse.getUsernameHash();
         final var hasServerUsername = !isEmpty(serverUsernameHash);
         final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
         final var serverUsernameHash = whoAmIResponse.getUsernameHash();
         final var hasServerUsername = !isEmpty(serverUsernameHash);
-        final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(new Username(localUsername).getHash());
+        final var username = new Username(localUsername);
+        final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash());
 
         if (!hasServerUsername) {
             logger.debug("No remote username is set.");
 
         if (!hasServerUsername) {
             logger.debug("No remote username is set.");
@@ -360,17 +370,40 @@ public class AccountHelper {
 
         if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
             logger.debug("Attempting to resynchronize username.");
 
         if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
             logger.debug("Attempting to resynchronize username.");
-            tryReserveConfirmUsername(localUsername, localUsernameHash);
+            try {
+                tryReserveConfirmUsername(username);
+            } catch (UsernameMalformedException | UsernameTakenException | UsernameIsNotReservedException e) {
+                logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
+                        e.getMessage(),
+                        e.getClass().getSimpleName());
+                account.setUsername(null);
+                account.setUsernameLink(null);
+                throw e;
+            }
         } else {
             logger.debug("Username already set, not refreshing.");
         }
     }
 
         } 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));
+    private void tryReserveConfirmUsername(final Username username) throws IOException {
+        final var response = dependencies.getAccountManager()
+                .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
         logger.debug("[reserveUsername] Successfully reserved existing username.");
         logger.debug("[reserveUsername] Successfully reserved existing username.");
-        dependencies.getAccountManager().confirmUsername(username, response);
+        dependencies.getAccountManager().confirmUsername(username.getUsername(), response);
         logger.debug("[confirmUsername] Successfully confirmed existing username.");
         logger.debug("[confirmUsername] Successfully confirmed existing username.");
+        tryToSetUsernameLink(username);
+    }
+
+    private void tryToSetUsernameLink(Username username) {
+        for (var i = 1; i < 4; i++) {
+            try {
+                final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
+                account.setUsernameLink(linkComponents);
+                break;
+            } catch (IOException e) {
+                logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e);
+            }
+        }
     }
 
     public void deleteUsername() throws IOException {
     }
 
     public void deleteUsername() throws IOException {
index 2ab177683caba0ddbab869038039c6b0d69533a3..6bddde847279dc91d63e4ff02f8366c58eaee3a8 100644 (file)
@@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper;
 
 import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.api.UnregisteredRecipientException;
 
 import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.api.UnregisteredRecipientException;
+import org.asamk.signal.manager.api.UsernameLinkUrl;
 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
 import org.asamk.signal.manager.internal.SignalDependencies;
 import org.asamk.signal.manager.storage.SignalAccount;
 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
 import org.asamk.signal.manager.internal.SignalDependencies;
 import org.asamk.signal.manager.storage.SignalAccount;
@@ -93,10 +94,23 @@ public class RecipientHelper {
                 }
             });
         } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
                 }
             });
         } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
-            final var username = usernameRecipient.username();
-            return account.getRecipientStore().resolveRecipientByUsername(username, () -> {
+            var username = usernameRecipient.username();
+            try {
+                UsernameLinkUrl usernameLinkUrl = UsernameLinkUrl.fromUri(username);
+                final var components = usernameLinkUrl.getComponents();
+                final var encryptedUsername = dependencies.getAccountManager()
+                        .getEncryptedUsernameFromLinkServerId(components.getServerId());
+                final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
+
+                username = Username.fromLink(link).getUsername();
+            } catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
+            } catch (IOException | BaseUsernameException e) {
+                throw new RuntimeException(e);
+            }
+            final String finalUsername = username;
+            return account.getRecipientStore().resolveRecipientByUsername(finalUsername, () -> {
                 try {
                 try {
-                    return getRegisteredUserByUsername(username);
+                    return getRegisteredUserByUsername(finalUsername);
                 } catch (Exception e) {
                     return null;
                 }
                 } catch (Exception e) {
                     return null;
                 }
index d4d0b10a9e50b3431997135b16ab2737acc905f8..cfa54cde26cc10d2d7e316c16124f5e76ab834d6 100644 (file)
@@ -61,6 +61,7 @@ import org.asamk.signal.manager.api.UnregisteredRecipientException;
 import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
 import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
+import org.asamk.signal.manager.api.UsernameLinkUrl;
 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
 import org.asamk.signal.manager.helper.AccountFileUpdater;
 import org.asamk.signal.manager.helper.Context;
 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
 import org.asamk.signal.manager.helper.AccountFileUpdater;
 import org.asamk.signal.manager.helper.Context;
@@ -332,9 +333,19 @@ public class ManagerImpl implements Manager {
     }
 
     @Override
     }
 
     @Override
-    public String setUsername(final String username) throws IOException, InvalidUsernameException {
+    public String getUsername() {
+        return account.getUsername();
+    }
+
+    @Override
+    public UsernameLinkUrl getUsernameLink() {
+        return new UsernameLinkUrl(account.getUsernameLink());
+    }
+
+    @Override
+    public void setUsername(final String username) throws IOException, InvalidUsernameException {
         try {
         try {
-            return context.getAccountHelper().reserveUsername(username);
+            context.getAccountHelper().reserveUsername(username);
         } catch (BaseUsernameException e) {
             throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
         }
         } catch (BaseUsernameException e) {
             throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
         }
index cd6720d3171d65ed912b483e8b3ed45274f7b877..5a6738d1bb7ec92e03ad1dbfa1f2d79e3ec3ec7f 100644 (file)
@@ -76,6 +76,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.ACI;
 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
 import org.whispersystems.signalservice.api.push.ServiceIdType;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
 import org.whispersystems.signalservice.api.push.ServiceIdType;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
 import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
 import org.whispersystems.signalservice.api.storage.StorageKey;
 import org.whispersystems.signalservice.api.util.CredentialsProvider;
 import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
 import org.whispersystems.signalservice.api.storage.StorageKey;
 import org.whispersystems.signalservice.api.util.CredentialsProvider;
@@ -129,6 +130,7 @@ public class SignalAccount implements Closeable {
     private ServiceEnvironment serviceEnvironment;
     private String number;
     private String username;
     private ServiceEnvironment serviceEnvironment;
     private String number;
     private String username;
+    private UsernameLinkComponents usernameLink;
     private String encryptedDeviceName;
     private int deviceId = 0;
     private String password;
     private String encryptedDeviceName;
     private int deviceId = 0;
     private String password;
@@ -1278,6 +1280,14 @@ public class SignalAccount implements Closeable {
         save();
     }
 
         save();
     }
 
+    public UsernameLinkComponents getUsernameLink() {
+        return usernameLink;
+    }
+
+    public void setUsernameLink(final UsernameLinkComponents usernameLink) {
+        this.usernameLink = usernameLink;
+    }
+
     public ServiceEnvironment getServiceEnvironment() {
         return serviceEnvironment;
     }
     public ServiceEnvironment getServiceEnvironment() {
         return serviceEnvironment;
     }
index d430f49fae581549ba5c1c14e872d3add96dd3fa..c729a39a81471c5c2f732eedfcf567055e8e40c5 100644 (file)
@@ -49,10 +49,15 @@ public class UpdateAccountCommand implements JsonRpcLocalCommand {
         var username = ns.getString("username");
         if (username != null) {
             try {
         var username = ns.getString("username");
         if (username != null) {
             try {
-                final var newUsername = m.setUsername(username);
+                m.setUsername(username);
+                final var newUsername = m.getUsername();
+                final var newUsernameLink = m.getUsernameLink();
                 switch (outputWriter) {
                 switch (outputWriter) {
-                    case PlainTextWriter w -> w.println("Your new username: {}", newUsername);
-                    case JsonWriter w -> w.write(new JsonAccountResponse(newUsername));
+                    case PlainTextWriter w -> w.println("Your new username: {} ({})",
+                            newUsername,
+                            newUsernameLink == null ? "-" : newUsernameLink.getUrl());
+                    case JsonWriter w -> w.write(new JsonAccountResponse(newUsername,
+                            newUsernameLink == null ? null : newUsernameLink.getUrl()));
                 }
             } catch (IOException e) {
                 throw new IOErrorException("Failed to set username: " + e.getMessage(), e);
                 }
             } catch (IOException e) {
                 throw new IOErrorException("Failed to set username: " + e.getMessage(), e);
@@ -72,6 +77,7 @@ public class UpdateAccountCommand implements JsonRpcLocalCommand {
     }
 
     private record JsonAccountResponse(
     }
 
     private record JsonAccountResponse(
-            @JsonInclude(JsonInclude.Include.NON_NULL) String username
+            @JsonInclude(JsonInclude.Include.NON_NULL) String username,
+            @JsonInclude(JsonInclude.Include.NON_NULL) String usernameLink
     ) {}
 }
     ) {}
 }
index a3f18393799ac234da463b76fdadac756a126123..985e2674d974c8030374c27eb5234a0175f49edf 100644 (file)
@@ -46,6 +46,7 @@ import org.asamk.signal.manager.api.UnregisteredRecipientException;
 import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
 import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
+import org.asamk.signal.manager.api.UsernameLinkUrl;
 import org.freedesktop.dbus.DBusMap;
 import org.freedesktop.dbus.DBusPath;
 import org.freedesktop.dbus.connections.impl.DBusConnection;
 import org.freedesktop.dbus.DBusMap;
 import org.freedesktop.dbus.DBusPath;
 import org.freedesktop.dbus.connections.impl.DBusConnection;
@@ -164,7 +165,17 @@ public class DbusManagerImpl implements Manager {
     }
 
     @Override
     }
 
     @Override
-    public String setUsername(final String username) throws IOException, InvalidUsernameException {
+    public String getUsername() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public UsernameLinkUrl getUsernameLink() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setUsername(final String username) throws IOException, InvalidUsernameException {
         throw new UnsupportedOperationException();
     }
 
         throw new UnsupportedOperationException();
     }