Extend getUserStatus command for usernames

This commit is contained in:
AsamK 2024-03-22 10:54:42 +01:00
parent 8b4f377cf1
commit d356d92b5e
8 changed files with 126 additions and 30 deletions

View file

@ -693,7 +693,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":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",

View 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.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl; 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.api.VerificationMethodNotAvailableException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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( void updateAccountAttributes(
String deviceName, String deviceName,
Boolean unrestrictedUnidentifiedSender, Boolean unrestrictedUnidentifiedSender,

View file

@ -0,0 +1,5 @@
package org.asamk.signal.manager.api;
import java.util.UUID;
public record UsernameStatus(String username, UUID uuid, boolean unrestrictedUnidentifiedAccess) {}

View file

@ -91,28 +91,51 @@ public class RecipientHelper {
}); });
} else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) { } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
var username = usernameRecipient.username(); var username = usernameRecipient.username();
try { return resolveRecipientByUsernameOrLink(username, false);
UsernameLinkUrl usernameLinkUrl = UsernameLinkUrl.fromUri(username); }
final var components = usernameLinkUrl.getComponents(); throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
final var encryptedUsername = dependencies.getAccountManager() }
.getEncryptedUsernameFromLinkServerId(components.getServerId());
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
username = Username.fromLink(link).getUsername(); public RecipientId resolveRecipientByUsernameOrLink(
} catch (UsernameLinkUrl.InvalidUsernameLinkException e) { String username, boolean forceRefresh
) throws UnregisteredRecipientException {
final Username finalUsername;
try {
finalUsername = getUsernameFromUsernameOrLink(username);
} catch (IOException | BaseUsernameException e) { } catch (IOException | BaseUsernameException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
final String finalUsername = username; if (forceRefresh) {
return account.getRecipientStore().resolveRecipientByUsername(finalUsername, () -> {
try { try {
return getRegisteredUserByUsername(finalUsername); 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));
}
}
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
try {
return dependencies.getAccountManager().getAciByUsername(finalUsername);
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
}); });
} }
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
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 record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
public RegisteredUser { public RegisteredUser {

View 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.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl; 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.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.helper.AccountFileUpdater; 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 @Override
public void updateAccountAttributes( public void updateAccountAttributes(
String deviceName, String deviceName,

View 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. Shows if they are registered on the Signal Servers or not.
In json mode this is outputted as a list of objects. In json mode this is outputted as a list of objects.
[NUMBER [NUMBER ...]]:: [NUMBER [NUMBER ...]]::
One or more numbers to check. One or more numbers to check.
[--username [USERNAME ...]]::
One or more usernames to check.
=== send === send
Send a message to another user or group. Send a message to another user or group.

View 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.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; 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.Manager;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UserStatus; 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.JsonWriter;
import org.asamk.signal.output.OutputWriter; import org.asamk.signal.output.OutputWriter;
import org.asamk.signal.output.PlainTextWriter; import org.asamk.signal.output.PlainTextWriter;
@ -19,6 +22,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; 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 @Override
public void attachToSubparser(final Subparser subparser) { public void attachToSubparser(final Subparser subparser) {
subparser.help("Check if the specified phone number/s have been registered"); 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 // Output
switch (outputWriter) { switch (outputWriter) {
case JsonWriter writer -> { 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 number = entry.getValue().number();
final var uuid = entry.getValue().uuid(); final var uuid = entry.getValue().uuid();
return new JsonUserStatus(entry.getKey(), return new JsonUserStatus(entry.getKey(),
number, number,
null,
uuid == null ? null : uuid.toString(), uuid == null ? null : uuid.toString(),
uuid != null); uuid != null);
}).toList(); }), 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);
})).toList();
writer.write(jsonUserStatuses); writer.write(jsonUserStatuses);
} }
case PlainTextWriter writer -> { case PlainTextWriter writer -> {
@ -75,9 +94,22 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
userStatus.uuid() != null, userStatus.uuid() != null,
userStatus.unrestrictedUnidentifiedAccess() ? " (unrestricted sealed sender)" : ""); 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
) {}
} }

View 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.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl; import org.asamk.signal.manager.api.UsernameLinkUrl;
import org.asamk.signal.manager.api.UsernameStatus;
import org.freedesktop.dbus.DBusMap; import org.freedesktop.dbus.DBusMap;
import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.connections.impl.DBusConnection; 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 @Override
public void updateAccountAttributes( public void updateAccountAttributes(
final String deviceName, final String deviceName,