]> nmode's Git Repositories - signal-cli/commitdiff
Extend getUserStatus command for usernames
authorAsamK <asamk@gmx.de>
Fri, 22 Mar 2024 09:54:42 +0000 (10:54 +0100)
committerAsamK <asamk@gmx.de>
Fri, 22 Mar 2024 09:54:42 +0000 (10:54 +0100)
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/UsernameStatus.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java
man/signal-cli.1.adoc
src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java
src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java

index 96961e4c2f9b3297f5db9b78c62eae771e1d4850..e3b47e86b0e4b4c7aa0690f735f04336fa29bb10 100644 (file)
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
   "allDeclaredConstructors":true,
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
   "allDeclaredConstructors":true,
-  "methods":[{"name":"isRegistered","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"recipient","parameterTypes":[] }, {"name":"uuid","parameterTypes":[] }]
+  "methods":[{"name":"isRegistered","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"recipient","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"uuid","parameterTypes":[] }]
 },
 {
   "name":"org.asamk.signal.commands.ListAccountsCommand$JsonAccount",
 },
 {
   "name":"org.asamk.signal.commands.ListAccountsCommand$JsonAccount",
index d6cf01d8834885d77c3dbfba6eff7fb50894ae6e..736f4e081893bd69890ae01ba63a0fb512d2e7b0 100644 (file)
@@ -44,6 +44,7 @@ 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.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
 import org.asamk.signal.manager.api.UsernameLinkUrl;
+import org.asamk.signal.manager.api.UsernameStatus;
 import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -92,6 +93,8 @@ public interface Manager extends Closeable {
      */
     Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
 
      */
     Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
 
+    Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames);
+
     void updateAccountAttributes(
             String deviceName,
             Boolean unrestrictedUnidentifiedSender,
     void updateAccountAttributes(
             String deviceName,
             Boolean unrestrictedUnidentifiedSender,
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UsernameStatus.java b/lib/src/main/java/org/asamk/signal/manager/api/UsernameStatus.java
new file mode 100644 (file)
index 0000000..570d551
--- /dev/null
@@ -0,0 +1,5 @@
+package org.asamk.signal.manager.api;
+
+import java.util.UUID;
+
+public record UsernameStatus(String username, UUID uuid, boolean unrestrictedUnidentifiedAccess) {}
index 905bbd5a3d67a848a1794dfd67c74c21c50848a9..1581c1a5112aa7bace07330de18ecfdfe6a364d5 100644 (file)
@@ -91,28 +91,51 @@ public class RecipientHelper {
             });
         } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
             var username = usernameRecipient.username();
             });
         } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
             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);
+            return resolveRecipientByUsernameOrLink(username, false);
+        }
+        throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
+    }
 
 
-                username = Username.fromLink(link).getUsername();
-            } catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
-            } catch (IOException | BaseUsernameException e) {
-                throw new RuntimeException(e);
+    public RecipientId resolveRecipientByUsernameOrLink(
+            String username, boolean forceRefresh
+    ) throws UnregisteredRecipientException {
+        final Username finalUsername;
+        try {
+            finalUsername = getUsernameFromUsernameOrLink(username);
+        } catch (IOException | BaseUsernameException e) {
+            throw new RuntimeException(e);
+        }
+        if (forceRefresh) {
+            try {
+                final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername);
+                return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
+            } catch (IOException e) {
+                throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
+                        null,
+                        username));
             }
             }
-            final String finalUsername = username;
-            return account.getRecipientStore().resolveRecipientByUsername(finalUsername, () -> {
-                try {
-                    return getRegisteredUserByUsername(finalUsername);
-                } catch (Exception e) {
-                    return null;
-                }
-            });
         }
         }
-        throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
+        return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
+            try {
+                return dependencies.getAccountManager().getAciByUsername(finalUsername);
+            } catch (Exception e) {
+                return null;
+            }
+        });
+    }
+
+    private Username getUsernameFromUsernameOrLink(String username) throws BaseUsernameException, IOException {
+        try {
+            final var 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);
+
+            return Username.fromLink(link);
+        } catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
+            return new Username(username);
+        }
     }
 
     public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
     }
 
     public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
@@ -246,10 +269,6 @@ public class RecipientHelper {
         return registeredUsers;
     }
 
         return registeredUsers;
     }
 
-    private ACI getRegisteredUserByUsername(String username) throws IOException, BaseUsernameException {
-        return dependencies.getAccountManager().getAciByUsername(new Username(username));
-    }
-
     public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
 
         public RegisteredUser {
     public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
 
         public RegisteredUser {
index 47a5bc031655b70162d9dcaa279e2842a8a6568b..3f99d8e6bf9d164e48695f2d7e94954a57bea251 100644 (file)
@@ -65,6 +65,7 @@ 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.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
 import org.asamk.signal.manager.api.UsernameLinkUrl;
+import org.asamk.signal.manager.api.UsernameStatus;
 import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
 import org.asamk.signal.manager.helper.AccountFileUpdater;
 import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
 import org.asamk.signal.manager.helper.AccountFileUpdater;
@@ -280,6 +281,33 @@ public class ManagerImpl implements Manager {
         }));
     }
 
         }));
     }
 
+    @Override
+    public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) {
+        final var registeredUsers = new HashMap<String, RecipientAddress>();
+        for (final var username : usernames) {
+            try {
+                final var recipientId = context.getRecipientHelper().resolveRecipientByUsernameOrLink(username, true);
+                final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
+                registeredUsers.put(username, address);
+            } catch (UnregisteredRecipientException e) {
+                // ignore
+            }
+        }
+
+        return usernames.stream().collect(Collectors.toMap(n -> n, username -> {
+            final var user = registeredUsers.get(username);
+            final var serviceId = user == null ? null : user.serviceId().orElse(null);
+            final var profile = serviceId == null
+                    ? null
+                    : context.getProfileHelper()
+                            .getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
+            return new UsernameStatus(username,
+                    serviceId == null ? null : serviceId.getRawUuid(),
+                    profile != null
+                            && profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED);
+        }));
+    }
+
     @Override
     public void updateAccountAttributes(
             String deviceName,
     @Override
     public void updateAccountAttributes(
             String deviceName,
index 80d4c37ef07d34efc49376c44ef28ada4d42d276..8739e493e45362f12f704b46acadcf0d38e077ae 100644 (file)
@@ -266,13 +266,16 @@ Use listDevices to see the deviceIds.
 
 === getUserStatus
 
 
 === getUserStatus
 
-Uses a list of phone numbers to determine the statuses of those users.
+Uses a list of phone numbers or usernames to determine the statuses of those users.
 Shows if they are registered on the Signal Servers or not.
 In json mode this is outputted as a list of objects.
 
 [NUMBER [NUMBER ...]]::
 One or more numbers to check.
 
 Shows if they are registered on the Signal Servers or not.
 In json mode this is outputted as a list of objects.
 
 [NUMBER [NUMBER ...]]::
 One or more numbers to check.
 
+[--username [USERNAME ...]]::
+One or more usernames to check.
+
 === send
 
 Send a message to another user or group.
 === send
 
 Send a message to another user or group.
index 7cf78ec8f230ffd102ce11a836e2ad036ec3b994..1691fac84066cc7d7ef974cf2b6c15c35859414a 100644 (file)
@@ -1,5 +1,7 @@
 package org.asamk.signal.commands;
 
 package org.asamk.signal.commands;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+
 import net.sourceforge.argparse4j.inf.Namespace;
 import net.sourceforge.argparse4j.inf.Subparser;
 
 import net.sourceforge.argparse4j.inf.Namespace;
 import net.sourceforge.argparse4j.inf.Subparser;
 
@@ -9,6 +11,7 @@ import org.asamk.signal.commands.exceptions.RateLimitErrorException;
 import org.asamk.signal.manager.Manager;
 import org.asamk.signal.manager.api.RateLimitException;
 import org.asamk.signal.manager.api.UserStatus;
 import org.asamk.signal.manager.Manager;
 import org.asamk.signal.manager.api.RateLimitException;
 import org.asamk.signal.manager.api.UserStatus;
+import org.asamk.signal.manager.api.UsernameStatus;
 import org.asamk.signal.output.JsonWriter;
 import org.asamk.signal.output.OutputWriter;
 import org.asamk.signal.output.PlainTextWriter;
 import org.asamk.signal.output.JsonWriter;
 import org.asamk.signal.output.OutputWriter;
 import org.asamk.signal.output.PlainTextWriter;
@@ -19,6 +22,7 @@ import org.slf4j.LoggerFactory;
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.Map;
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.stream.Stream;
 
 public class GetUserStatusCommand implements JsonRpcLocalCommand {
 
 
 public class GetUserStatusCommand implements JsonRpcLocalCommand {
 
@@ -32,7 +36,8 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
     @Override
     public void attachToSubparser(final Subparser subparser) {
         subparser.help("Check if the specified phone number/s have been registered");
     @Override
     public void attachToSubparser(final Subparser subparser) {
         subparser.help("Check if the specified phone number/s have been registered");
-        subparser.addArgument("recipient").help("Phone number").nargs("+");
+        subparser.addArgument("recipient").help("Phone number").nargs("*");
+        subparser.addArgument("--username").help("Specify the recipient username or username link.").nargs("*");
     }
 
     @Override
     }
 
     @Override
@@ -54,17 +59,31 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
                     + ")", e);
         }
 
                     + ")", e);
         }
 
+        final var usernames = ns.<String>getList("username");
+        final var registeredUsernames = usernames == null
+                ? Map.<String, UsernameStatus>of()
+                : m.getUsernameStatus(new HashSet<>(usernames));
+
         // Output
         switch (outputWriter) {
             case JsonWriter writer -> {
         // Output
         switch (outputWriter) {
             case JsonWriter writer -> {
-                var jsonUserStatuses = registered.entrySet().stream().map(entry -> {
+                var jsonUserStatuses = Stream.concat(registered.entrySet().stream().map(entry -> {
                     final var number = entry.getValue().number();
                     final var uuid = entry.getValue().uuid();
                     return new JsonUserStatus(entry.getKey(),
                             number,
                     final var number = entry.getValue().number();
                     final var uuid = entry.getValue().uuid();
                     return new JsonUserStatus(entry.getKey(),
                             number,
+                            null,
+                            uuid == null ? null : uuid.toString(),
+                            uuid != null);
+                }), registeredUsernames.entrySet().stream().map(entry -> {
+                    final var username = entry.getValue().username();
+                    final var uuid = entry.getValue().uuid();
+                    return new JsonUserStatus(entry.getKey(),
+                            null,
+                            username,
                             uuid == null ? null : uuid.toString(),
                             uuid != null);
                             uuid == null ? null : uuid.toString(),
                             uuid != null);
-                }).toList();
+                })).toList();
                 writer.write(jsonUserStatuses);
             }
             case PlainTextWriter writer -> {
                 writer.write(jsonUserStatuses);
             }
             case PlainTextWriter writer -> {
@@ -75,9 +94,22 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
                             userStatus.uuid() != null,
                             userStatus.unrestrictedUnidentifiedAccess() ? " (unrestricted sealed sender)" : "");
                 }
                             userStatus.uuid() != null,
                             userStatus.unrestrictedUnidentifiedAccess() ? " (unrestricted sealed sender)" : "");
                 }
+                for (var entry : registeredUsernames.entrySet()) {
+                    final var userStatus = entry.getValue();
+                    writer.println("{}: {}{}",
+                            entry.getKey(),
+                            userStatus.uuid() != null,
+                            userStatus.unrestrictedUnidentifiedAccess() ? " (unrestricted sealed sender)" : "");
+                }
             }
         }
     }
 
             }
         }
     }
 
-    private record JsonUserStatus(String recipient, String number, String uuid, boolean isRegistered) {}
+    private record JsonUserStatus(
+            String recipient,
+            @JsonInclude(JsonInclude.Include.NON_NULL) String number,
+            @JsonInclude(JsonInclude.Include.NON_NULL) String username,
+            String uuid,
+            boolean isRegistered
+    ) {}
 }
 }
index 522bb3f6bd9414c25263a1234160ba29e0774c47..cd65be5105fd1ff78117f27b06ac52a3698bfe26 100644 (file)
@@ -47,6 +47,7 @@ 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.api.UpdateProfile;
 import org.asamk.signal.manager.api.UserStatus;
 import org.asamk.signal.manager.api.UsernameLinkUrl;
+import org.asamk.signal.manager.api.UsernameStatus;
 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;
@@ -122,6 +123,11 @@ public class DbusManagerImpl implements Manager {
         return result;
     }
 
         return result;
     }
 
+    @Override
+    public Map<String, UsernameStatus> getUsernameStatus(final Set<String> usernames) {
+        throw new UnsupportedOperationException();
+    }
+
     @Override
     public void updateAccountAttributes(
             final String deviceName,
     @Override
     public void updateAccountAttributes(
             final String deviceName,