mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-10-21 07:59:32 +00:00
Support numberless accounts over D-Bus
This commit is contained in:
parent
29140bbf64
commit
f85763b807
@ -27,9 +27,22 @@ Receive events use the ACI in their `account` field when there is no phone numbe
|
||||
REST wrappers must understand those nullable numbers and ACI selectors before they
|
||||
can be assumed compatible.
|
||||
|
||||
Numberless accounts are supported through the CLI and JSON-RPC interfaces. The
|
||||
D-Bus interface still assumes phone-number-based account identifiers and does not
|
||||
support numberless accounts.
|
||||
Numberless accounts are also supported through D-Bus. Start
|
||||
`signal-cli daemon --dbus`, then use the ACI with the D-Bus client:
|
||||
|
||||
```sh
|
||||
signal-cli --dbus -a YOUR_ACCOUNT_ACI listGroups
|
||||
signal-cli --dbus -a YOUR_ACCOUNT_ACI send --note-to-self -m 'Numberless account test'
|
||||
```
|
||||
|
||||
In multi-account mode, D-Bus exports numberless accounts under
|
||||
`/org/asamk/Signal/ACI`, with the ACI's hyphens replaced by underscores. Numbered
|
||||
accounts retain their phone-number-based paths. Use `SignalControl.listAccounts`
|
||||
or `SignalControl.getAccount` to discover paths; `getAccount` accepts either a
|
||||
phone number or an ACI. `Signal.getSelfNumber` returns an empty string when the
|
||||
account has no phone number, and `Signal.getSelfACI` returns the ACI.
|
||||
`Signal.listRecipientIdentifiers` includes contacts without a phone number;
|
||||
`Signal.listNumbers` continues to return only phone numbers.
|
||||
|
||||
## Validation
|
||||
|
||||
@ -38,6 +51,13 @@ request (including ACI authentication and omission of PNI keys), numbered-accoun
|
||||
linking, missing group credential salts, persisted account reload, account discovery,
|
||||
group membership lookup, recipient-number formatting, and JSON account identifiers.
|
||||
|
||||
D-Bus transport tests require a session bus. To run them on an isolated bus, use
|
||||
`dbus-run-session -- ./gradlew --no-daemon :test --tests '*NumberlessDbusTest*' --rerun-tasks`.
|
||||
These tests exercise the D-Bus server and client with a local test backend, including
|
||||
mixed accounts, direct and group messages, self-directed operations, and account
|
||||
removal. They do not contact Signal. Without a session bus, only the D-Bus unit
|
||||
tests run and the transport tests are skipped.
|
||||
|
||||
Live acceptance requires a real mobile account and is separate from these tests:
|
||||
|
||||
1. Link a newly created secondary device using the numberless mobile account.
|
||||
@ -45,6 +65,8 @@ Live acceptance requires a real mobile account and is separate from these tests:
|
||||
3. Send and receive a Note to Self message and a DM with a second account.
|
||||
4. Receive a group message, list the group, and send a reply to that test group.
|
||||
5. Run the JSON-RPC daemon, restart it, and repeat receiving and sending by ACI.
|
||||
6. Repeat with `daemon --dbus` and the `--dbus -a YOUR_ACCOUNT_ACI` client, checking
|
||||
direct messages, group messages, and Note to Self.
|
||||
|
||||
Only one signal-cli process may open an account's state at a time. Stop the daemon
|
||||
before running one-off commands against the same configuration directory.
|
||||
|
||||
@ -15,9 +15,13 @@ signal-cli-dbus - A commandline and dbus interface for the Signal messenger
|
||||
|
||||
*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system]
|
||||
|
||||
*dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/_<phonenumber>] org.asamk.Signal.<method> [string:<string argument>] [array:<type>:<array argument>]
|
||||
*dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/<account-id>] org.asamk.Signal.<method> [string:<string argument>] [array:<type>:<array argument>]
|
||||
|
||||
Note: when daemon was started without explicit `-a ACCOUNT`, the `dbus-send` command requires adding the phone number in `/org/asamk/Signal/_<phonenumber>`.
|
||||
Note: when daemon was started without explicit `-a ACCOUNT`, use the account's
|
||||
object path returned by `org.asamk.SignalControl.listAccounts` or
|
||||
`org.asamk.SignalControl.getAccount`. Numbered accounts use
|
||||
`/org/asamk/Signal/_<phonenumber>`. Accounts without a phone number use
|
||||
`/org/asamk/Signal/<ACI>`, with hyphens replaced by underscores.
|
||||
|
||||
== Description
|
||||
|
||||
@ -49,6 +53,11 @@ All Exceptions begin with "org.asamk.Signal.Error." which is omitted here for be
|
||||
|
||||
Phone numbers always have the format +<countrycode><regional number>
|
||||
|
||||
For an account without a phone number, use its ACI (Account ID) with the
|
||||
`signal-cli --dbus -a ACCOUNT` client. ACI selectors also work for numbered accounts.
|
||||
An account without a phone number cannot infer the country code of a recipient;
|
||||
use a complete international phone number, an ACI, or a Signal username.
|
||||
|
||||
== Methods
|
||||
|
||||
=== SignalControl interface
|
||||
@ -73,11 +82,29 @@ For example:
|
||||
dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client" | tr '\n' '\0' | sed 's/.*string //g' | sed 's/\"//g' | qrencode -s10 -tANSI256
|
||||
----
|
||||
|
||||
listAccounts() -> accountList<as>::
|
||||
listAccounts() -> accountList<ao>::
|
||||
* accountList : Array of all attached accounts in DBus object path form
|
||||
|
||||
Exceptions: None
|
||||
|
||||
getAccount(identifier<s>) -> account<o>::
|
||||
* identifier : Phone number or ACI of an attached account
|
||||
* account : DBus object path of the account
|
||||
|
||||
Exceptions: org.asamk.SignalControl.Error.Failure if the account is unknown
|
||||
|
||||
startLink() -> deviceLinkUri<s>::
|
||||
* deviceLinkUri : Provisioning URI to scan with the primary Signal device
|
||||
|
||||
finishLink(deviceLinkUri<s>, newDeviceName<s>) -> identifier<s>::
|
||||
* deviceLinkUri : URI returned by startLink
|
||||
* newDeviceName : Name for the linked device
|
||||
* identifier : Phone number of the linked account, or its ACI when it has no phone number
|
||||
|
||||
Both linking methods support accounts with or without a phone number.
|
||||
|
||||
Exceptions: org.asamk.SignalControl.Error.Failure
|
||||
|
||||
register(number<s>, voiceVerification<b>) -> <>::
|
||||
* number : Phone number
|
||||
* voiceVerification : true = use voice verification; false = use SMS verification
|
||||
@ -115,9 +142,10 @@ Exceptions: None
|
||||
|
||||
The following methods listen to the account's object path, which is constructed as follows:
|
||||
|
||||
- for single-account mode: "/org/asamk/Signal/"
|
||||
- for multi-account mode: "/org/asamk/Signal/" + DBusNumber
|
||||
* DBusNumber: account's phone number, with underscore (_) replacing plus (+)
|
||||
- for single-account mode: "/org/asamk/Signal"
|
||||
- for multi-account mode: "/org/asamk/Signal/" + DBusIdentifier
|
||||
* DBusIdentifier: account's phone number with underscore (_) replacing plus (+),
|
||||
or its ACI with underscores replacing hyphens when there is no phone number
|
||||
|
||||
getContactName(number<s>) -> name<s>::
|
||||
* number : Phone number
|
||||
@ -135,7 +163,12 @@ May result in e.g. two entries if a contact and profile name is set.
|
||||
Exceptions: None
|
||||
|
||||
getSelfNumber() -> number<s>::
|
||||
* number : Your phone number
|
||||
* number : Your phone number, or an empty string for an account without a phone number
|
||||
|
||||
Exceptions: None
|
||||
|
||||
getSelfACI() -> aci<s>::
|
||||
* aci : Your account's ACI (Account ID), available with or without a phone number
|
||||
|
||||
Exceptions: None
|
||||
|
||||
@ -167,6 +200,14 @@ This is a concatenated list of all defined contacts as well of profiles known (e
|
||||
|
||||
Exceptions: None
|
||||
|
||||
listRecipientIdentifiers() -> identifiers<as>::
|
||||
* identifiers : String array of known recipient identifiers, including recipients without a phone number
|
||||
|
||||
Uses phone numbers where known and includes ACIs for recipients without a phone
|
||||
number. Unlike `listNumbers`, this method also includes numberless recipients.
|
||||
|
||||
Exceptions: None
|
||||
|
||||
removePin() -> <>::
|
||||
|
||||
Removes registration PIN protection.
|
||||
|
||||
@ -20,6 +20,9 @@ import java.util.Map;
|
||||
*/
|
||||
public interface Signal extends DBusInterface {
|
||||
|
||||
/**
|
||||
* Returns the account's phone number, or an empty string for a numberless account.
|
||||
*/
|
||||
String getSelfNumber();
|
||||
|
||||
String getSelfACI();
|
||||
@ -186,6 +189,8 @@ public interface Signal extends DBusInterface {
|
||||
|
||||
List<String> listNumbers();
|
||||
|
||||
List<String> listRecipientIdentifiers();
|
||||
|
||||
List<String> getContactNumber(final String name) throws Error.Failure;
|
||||
|
||||
@Deprecated
|
||||
|
||||
@ -37,7 +37,10 @@ public interface SignalControl extends DBusInterface {
|
||||
|
||||
List<DBusPath> listAccounts();
|
||||
|
||||
DBusPath getAccount(String number);
|
||||
/**
|
||||
* Resolves a phone number or ACI to an exported account's object path.
|
||||
*/
|
||||
DBusPath getAccount(String identifier) throws Error.Failure;
|
||||
|
||||
interface Error {
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ public class DbusCommandHandler {
|
||||
try {
|
||||
if (command instanceof ProvisioningCommand c) {
|
||||
if (account != null) {
|
||||
throw new UserErrorException("You cannot specify a account (phone number) when linking");
|
||||
throw new UserErrorException("You cannot specify an account when linking");
|
||||
}
|
||||
|
||||
handleProvisioningCommand(c, dBusConn, busname, commandHandler);
|
||||
@ -74,7 +74,9 @@ public class DbusCommandHandler {
|
||||
throw new UserErrorException("Command only works in multi-account mode");
|
||||
}
|
||||
|
||||
var accountObjectPath = account == null ? tryGetSingleAccountObjectPath(dBusConn, busname) : null;
|
||||
var accountObjectPath = account == null
|
||||
? tryGetSingleAccountObjectPath(dBusConn, busname)
|
||||
: getAccountObjectPath(account, dBusConn, busname);
|
||||
if (accountObjectPath == null) {
|
||||
accountObjectPath = DbusConfig.getObjectPath(account);
|
||||
}
|
||||
@ -86,6 +88,24 @@ public class DbusCommandHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static String getAccountObjectPath(
|
||||
final String account,
|
||||
final DBusConnection dBusConn,
|
||||
final String busname
|
||||
) throws DBusException, CommandException {
|
||||
final var control = dBusConn.getRemoteObject(busname, DbusConfig.getObjectPath(), SignalControl.class);
|
||||
try {
|
||||
return control.getAccount(account).getPath();
|
||||
} catch (UnknownMethod e) {
|
||||
// A single-account daemon exports Signal directly at the base path.
|
||||
final var signal = dBusConn.getRemoteObject(busname, DbusConfig.getObjectPath(), Signal.class);
|
||||
if (!account.equals(signal.getSelfNumber()) && !account.equals(signal.getSelfACI())) {
|
||||
throw new UserErrorException("Unknown account: " + account);
|
||||
}
|
||||
return DbusConfig.getObjectPath();
|
||||
}
|
||||
}
|
||||
|
||||
private static String tryGetSingleAccountObjectPath(
|
||||
final DBusConnection dBusConn,
|
||||
final String busname
|
||||
@ -97,7 +117,7 @@ public class DbusCommandHandler {
|
||||
throw new UserErrorException("No local users found, you first need to register or link an account");
|
||||
} else if (accounts.size() > 1) {
|
||||
throw new UserErrorException(
|
||||
"Multiple users found, you need to specify an account (phone number) with -a");
|
||||
"Multiple users found, you need to specify an account (phone number or ACI) with -a");
|
||||
}
|
||||
|
||||
return accounts.getFirst().getPath();
|
||||
|
||||
@ -67,7 +67,7 @@ public class DbusHandler implements AutoCloseable {
|
||||
}
|
||||
});
|
||||
c.addOnManagerRemovedHandler(m -> {
|
||||
final var path = DbusConfig.getObjectPath(m.getSelfNumber());
|
||||
final var path = DbusConfig.getObjectPath(m.getSelfIdentifier());
|
||||
try {
|
||||
final var object = connection.getExportedObject(null, path);
|
||||
if (object instanceof DbusSignalImpl dbusSignal) {
|
||||
@ -135,11 +135,11 @@ public class DbusHandler implements AutoCloseable {
|
||||
final var signal = new DbusSignalImpl(m, conn, objectPath, noReceiveOnStart);
|
||||
closeables.add(signal);
|
||||
|
||||
return Thread.ofPlatform().name("dbus-init-" + m.getSelfNumber()).start(signal::initObjects);
|
||||
return Thread.ofPlatform().name("dbus-init-" + m.getSelfIdentifier()).start(signal::initObjects);
|
||||
}
|
||||
|
||||
private Thread exportManager(final DBusConnection conn, final Manager m) {
|
||||
final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber());
|
||||
final var objectPath = DbusConfig.getObjectPath(m.getSelfIdentifier());
|
||||
return exportDbusObject(conn, objectPath, m);
|
||||
}
|
||||
|
||||
|
||||
@ -56,6 +56,7 @@ import org.asamk.signal.manager.api.UsernameLinkUrl;
|
||||
import org.asamk.signal.manager.api.UsernameStatus;
|
||||
import org.freedesktop.dbus.DBusPath;
|
||||
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
||||
import org.freedesktop.dbus.errors.UnknownMethod;
|
||||
import org.freedesktop.dbus.exceptions.DBusException;
|
||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||
import org.freedesktop.dbus.interfaces.DBusInterface;
|
||||
@ -77,6 +78,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
@ -110,7 +112,8 @@ public class DbusManagerImpl implements Manager {
|
||||
|
||||
@Override
|
||||
public String getSelfNumber() {
|
||||
return signal.getSelfNumber();
|
||||
final var number = signal.getSelfNumber();
|
||||
return number.isEmpty() ? null : number;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -438,7 +441,7 @@ public class DbusManagerImpl implements Manager {
|
||||
numbers.forEach(n -> signal.sendTyping(n, action == TypingAction.STOP));
|
||||
return 0L;
|
||||
}, () -> {
|
||||
signal.sendTyping(signal.getSelfNumber(), action == TypingAction.STOP);
|
||||
signal.sendTyping(getSelfIdentifier(), action == TypingAction.STOP);
|
||||
return 0L;
|
||||
}, groupId -> {
|
||||
signal.sendGroupTyping(groupId, action == TypingAction.STOP);
|
||||
@ -486,7 +489,7 @@ public class DbusManagerImpl implements Manager {
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
return handleMessage(recipients,
|
||||
numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
|
||||
() -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
|
||||
() -> signal.sendRemoteDeleteMessage(targetSentTimestamp, getSelfIdentifier()),
|
||||
groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
|
||||
}
|
||||
|
||||
@ -510,7 +513,7 @@ public class DbusManagerImpl implements Manager {
|
||||
remove,
|
||||
targetAuthor.getIdentifier(),
|
||||
targetSentTimestamp,
|
||||
signal.getSelfNumber()),
|
||||
getSelfIdentifier()),
|
||||
groupId -> signal.sendGroupMessageReaction(emoji,
|
||||
remove,
|
||||
targetAuthor.getIdentifier(),
|
||||
@ -809,43 +812,46 @@ public class DbusManagerImpl implements Manager {
|
||||
final Collection<RecipientIdentifier.Single> addresses,
|
||||
final Optional<String> name
|
||||
) {
|
||||
final var numbers = addresses.stream()
|
||||
.filter(s -> s instanceof RecipientIdentifier.Number)
|
||||
.map(s -> ((RecipientIdentifier.Number) s).number())
|
||||
final var identifiers = addresses.stream()
|
||||
.map(RecipientIdentifier.Single::getIdentifier)
|
||||
.collect(Collectors.toSet());
|
||||
return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
|
||||
final var contactBlocked = signal.isContactBlocked(n);
|
||||
if (blocked.isPresent() && blocked.get() != contactBlocked) {
|
||||
return null;
|
||||
}
|
||||
final var contactName = signal.getContactName(n);
|
||||
if (onlyContacts && contactName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.isPresent() && !name.get().equals(contactName)) {
|
||||
return null;
|
||||
}
|
||||
return Recipient.newBuilder()
|
||||
.withAddress(new RecipientAddress(n))
|
||||
.withContact(new Contact(contactName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false,
|
||||
contactBlocked,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null))
|
||||
.build();
|
||||
}).filter(Objects::nonNull).toList();
|
||||
return listRecipientIdentifiers().stream()
|
||||
.filter(n -> addresses.isEmpty() || identifiers.contains(n))
|
||||
.map(n -> {
|
||||
final var contactBlocked = signal.isContactBlocked(n);
|
||||
if (blocked.isPresent() && blocked.get() != contactBlocked) {
|
||||
return null;
|
||||
}
|
||||
final var contactName = signal.getContactName(n);
|
||||
if (onlyContacts && contactName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.isPresent() && !name.get().equals(contactName)) {
|
||||
return null;
|
||||
}
|
||||
return Recipient.newBuilder()
|
||||
.withAddress(getRecipientAddress(n))
|
||||
.withContact(new Contact(contactName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false,
|
||||
contactBlocked,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null))
|
||||
.build();
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -870,16 +876,16 @@ public class DbusManagerImpl implements Manager {
|
||||
(String) group.get("Description").getValue(),
|
||||
GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
|
||||
((List<String>) group.get("Members").getValue()).stream()
|
||||
.map(m -> new GroupMember(new RecipientAddress(m), admins.contains(m), null, null))
|
||||
.map(m -> new GroupMember(getRecipientAddress(m), admins.contains(m), null, null))
|
||||
.collect(Collectors.toSet()),
|
||||
((List<String>) group.get("PendingMembers").getValue()).stream()
|
||||
.map(RecipientAddress::new)
|
||||
.map(DbusManagerImpl::getRecipientAddress)
|
||||
.collect(Collectors.toSet()),
|
||||
((List<String>) group.get("RequestingMembers").getValue()).stream()
|
||||
.map(RecipientAddress::new)
|
||||
.map(DbusManagerImpl::getRecipientAddress)
|
||||
.collect(Collectors.toSet()),
|
||||
((List<String>) group.get("Banned").getValue()).stream()
|
||||
.map(RecipientAddress::new)
|
||||
.map(DbusManagerImpl::getRecipientAddress)
|
||||
.collect(Collectors.toSet()),
|
||||
(boolean) group.get("IsBlocked").getValue(),
|
||||
(int) group.get("MessageExpirationTimer").getValue(),
|
||||
@ -910,7 +916,10 @@ public class DbusManagerImpl implements Manager {
|
||||
final var group = getRemoteObject(identityPath, Signal.Identity.class).GetAll("org.asamk.Signal.Identity");
|
||||
final var aci = (String) group.get("Uuid").getValue();
|
||||
final var number = (String) group.get("Number").getValue();
|
||||
return new Identity(new RecipientAddress(aci, null, number, null),
|
||||
return new Identity(new RecipientAddress(aci.isEmpty() ? null : aci,
|
||||
null,
|
||||
number.isEmpty() ? null : number,
|
||||
null),
|
||||
(byte[]) group.get("Fingerprint").getValue(),
|
||||
(String) group.get("SafetyNumber").getValue(),
|
||||
(byte[]) group.get("ScannableSafetyNumber").getValue(),
|
||||
@ -1073,6 +1082,23 @@ public class DbusManagerImpl implements Manager {
|
||||
return string == null ? "" : string;
|
||||
}
|
||||
|
||||
private static RecipientAddress getRecipientAddress(final String identifier) {
|
||||
try {
|
||||
return new RecipientAddress(UUID.fromString(identifier));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return new RecipientAddress(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> listRecipientIdentifiers() {
|
||||
try {
|
||||
return signal.listRecipientIdentifiers();
|
||||
} catch (UnknownMethod e) {
|
||||
// Older daemons only expose recipients with a known phone number.
|
||||
return signal.listNumbers();
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
|
||||
try {
|
||||
return connection.getRemoteObject(busname, path.getPath(), type);
|
||||
@ -1085,7 +1111,7 @@ public class DbusManagerImpl implements Manager {
|
||||
try {
|
||||
this.dbusMsgHandler = messageReceived -> {
|
||||
final var extras = messageReceived.getExtras();
|
||||
final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(messageReceived.getSender())),
|
||||
final var envelope = new MessageEnvelope(Optional.of(getRecipientAddress(messageReceived.getSender())),
|
||||
0,
|
||||
messageReceived.getTimestamp(),
|
||||
0,
|
||||
@ -1132,7 +1158,7 @@ public class DbusManagerImpl implements Manager {
|
||||
connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
|
||||
this.dbusEditMsgHandler = messageReceived -> {
|
||||
final var extras = messageReceived.getExtras();
|
||||
final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(messageReceived.getSender())),
|
||||
final var envelope = new MessageEnvelope(Optional.of(getRecipientAddress(messageReceived.getSender())),
|
||||
0,
|
||||
messageReceived.getTimestamp(),
|
||||
0,
|
||||
@ -1186,7 +1212,7 @@ public class DbusManagerImpl implements Manager {
|
||||
case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
|
||||
default -> MessageEnvelope.Receipt.Type.UNKNOWN;
|
||||
};
|
||||
final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(receiptReceived.getSender())),
|
||||
final var envelope = new MessageEnvelope(Optional.of(getRecipientAddress(receiptReceived.getSender())),
|
||||
0,
|
||||
receiptReceived.getTimestamp(),
|
||||
0,
|
||||
@ -1207,7 +1233,7 @@ public class DbusManagerImpl implements Manager {
|
||||
|
||||
this.dbusSyncHandler = syncReceived -> {
|
||||
final var extras = syncReceived.getExtras();
|
||||
final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(syncReceived.getSource())),
|
||||
final var envelope = new MessageEnvelope(Optional.of(getRecipientAddress(syncReceived.getSource())),
|
||||
0,
|
||||
syncReceived.getTimestamp(),
|
||||
0,
|
||||
@ -1221,7 +1247,7 @@ public class DbusManagerImpl implements Manager {
|
||||
syncReceived.getTimestamp(),
|
||||
syncReceived.getDestination().isEmpty()
|
||||
? Optional.empty()
|
||||
: Optional.of(new RecipientAddress(syncReceived.getDestination())),
|
||||
: Optional.of(getRecipientAddress(syncReceived.getDestination())),
|
||||
Set.of(),
|
||||
Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
|
||||
syncReceived.getGroupId().length > 0
|
||||
@ -1326,7 +1352,7 @@ public class DbusManagerImpl implements Manager {
|
||||
|
||||
final List<Map<String, Variant<?>>> mentions = getValue(extras, "mentions");
|
||||
return mentions.stream()
|
||||
.map(a -> new MessageEnvelope.Data.Mention(new RecipientAddress(this.<String>getValue(a, "recipient")),
|
||||
.map(a -> new MessageEnvelope.Data.Mention(getRecipientAddress(this.<String>getValue(a, "recipient")),
|
||||
getValue(a, "start"),
|
||||
getValue(a, "length")))
|
||||
.toList();
|
||||
|
||||
@ -45,10 +45,7 @@ public class DbusMultiAccountManagerImpl implements MultiAccountManager {
|
||||
|
||||
@Override
|
||||
public List<String> getAccountNumbers() {
|
||||
return signalControl.listAccounts()
|
||||
.stream()
|
||||
.map(a -> getRemoteObject(a, Signal.class).getSelfNumber())
|
||||
.toList();
|
||||
return getManagers().stream().map(Manager::getSelfNumber).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -74,8 +71,8 @@ public class DbusMultiAccountManagerImpl implements MultiAccountManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manager getManager(final String phoneNumber) {
|
||||
return new DbusManagerImpl(getRemoteObject(signalControl.getAccount(phoneNumber), Signal.class),
|
||||
public Manager getManager(final String identifier) {
|
||||
return new DbusManagerImpl(getRemoteObject(signalControl.getAccount(identifier), Signal.class),
|
||||
connection,
|
||||
busname);
|
||||
}
|
||||
|
||||
@ -159,11 +159,19 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
|
||||
|
||||
@Override
|
||||
public List<DBusPath> listAccounts() {
|
||||
return c.getAccountNumbers().stream().map(u -> new DBusPath(DbusConfig.getObjectPath(u))).toList();
|
||||
return c.getManagers()
|
||||
.stream()
|
||||
.map(m -> new DBusPath(DbusConfig.getObjectPath(m.getSelfIdentifier())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBusPath getAccount(final String number) {
|
||||
return new DBusPath(DbusConfig.getObjectPath(number));
|
||||
public DBusPath getAccount(final String identifier) {
|
||||
final var manager = c.getManagers()
|
||||
.stream()
|
||||
.filter(m -> identifier.equals(m.getSelfNumber()) || identifier.equals(m.getSelfACI()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new Error.Failure("Unknown account: " + identifier));
|
||||
return new DBusPath(DbusConfig.getObjectPath(manager.getSelfIdentifier()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,7 +138,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||
|
||||
@Override
|
||||
public String getSelfNumber() {
|
||||
return m.getSelfNumber();
|
||||
return emptyIfNull(m.getSelfNumber());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -803,6 +803,15 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listRecipientIdentifiers() {
|
||||
return m.getRecipients(false, Optional.empty(), Set.of(), Optional.empty())
|
||||
.stream()
|
||||
.map(r -> r.getAddress().getLegacyIdentifier())
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getContactNumber(final String name) {
|
||||
return m.getRecipients(false, Optional.empty(), Set.of(), Optional.of(name))
|
||||
|
||||
435
src/test/java/org/asamk/signal/dbus/NumberlessDbusTest.java
Normal file
435
src/test/java/org/asamk/signal/dbus/NumberlessDbusTest.java
Normal file
@ -0,0 +1,435 @@
|
||||
package org.asamk.signal.dbus;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.SignalControl;
|
||||
import org.asamk.signal.DbusConfig;
|
||||
import org.asamk.signal.commands.CommandHandler;
|
||||
import org.asamk.signal.commands.ListGroupsCommand;
|
||||
import org.asamk.signal.commands.LocalCommand;
|
||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.api.Group;
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.api.GroupMember;
|
||||
import org.asamk.signal.manager.api.GroupPermission;
|
||||
import org.asamk.signal.manager.api.Identity;
|
||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||
import org.asamk.signal.manager.api.Recipient;
|
||||
import org.asamk.signal.manager.api.RecipientAddress;
|
||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||
import org.asamk.signal.manager.api.SendMessageResults;
|
||||
import org.asamk.signal.manager.api.TrustLevel;
|
||||
import org.asamk.signal.manager.api.TypingAction;
|
||||
import org.asamk.signal.manager.internal.MultiAccountManagerImpl;
|
||||
import org.freedesktop.dbus.DBusPath;
|
||||
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
||||
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
|
||||
import org.freedesktop.dbus.errors.UnknownMethod;
|
||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class NumberlessDbusTest {
|
||||
|
||||
private static final String NUMBER = "+12025550123";
|
||||
private static final String ACI = "11111111-1111-4111-8111-111111111111";
|
||||
private static final String OTHER_ACI = "22222222-2222-4222-8222-222222222222";
|
||||
private static final String THIRD_ACI = "33333333-3333-4333-8333-333333333333";
|
||||
private static final String NUMBERLESS_PATH = "/org/asamk/Signal/11111111_1111_4111_8111_111111111111";
|
||||
private static final String NUMBERED_PATH = "/org/asamk/Signal/_12025550123";
|
||||
|
||||
@Test
|
||||
void resolvesNumberlessAndNumberedAccountsWithoutChangingNumberedPaths() {
|
||||
final var numberless = new TestAccount(null, ACI);
|
||||
final var numbered = new TestAccount(NUMBER, OTHER_ACI);
|
||||
final var accounts = new MultiAccountManagerImpl(List.of(numberless.manager, numbered.manager), null);
|
||||
final var control = new DbusSignalControlImpl(accounts, DbusConfig.getObjectPath());
|
||||
|
||||
assertEquals(Set.of(new DBusPath(NUMBERLESS_PATH), new DBusPath(NUMBERED_PATH)),
|
||||
Set.copyOf(control.listAccounts()));
|
||||
assertEquals(NUMBERLESS_PATH, control.getAccount(ACI).getPath());
|
||||
assertEquals(NUMBERED_PATH, control.getAccount(NUMBER).getPath());
|
||||
assertEquals(NUMBERED_PATH, control.getAccount(OTHER_ACI).getPath());
|
||||
assertThrows(SignalControl.Error.Failure.class, () -> control.getAccount(THIRD_ACI));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
@ValueSource(strings = NUMBER)
|
||||
void mapsOptionalPhoneNumbersAndAddressesSelfByAnAvailableIdentifier(final String number) throws Exception {
|
||||
final var account = new TestAccount(number, ACI);
|
||||
final var signal = new DbusSignalImpl(account.manager, null, DbusConfig.getObjectPath(), true);
|
||||
final var client = new DbusManagerImpl(signal, null, "org.asamk.Signal.Test");
|
||||
|
||||
assertEquals(number == null ? "" : number, signal.getSelfNumber());
|
||||
assertEquals(number, client.getSelfNumber());
|
||||
assertEquals(number == null ? ACI : number, client.getSelfIdentifier());
|
||||
checkSelfOperations(client,
|
||||
account,
|
||||
number == null
|
||||
? new RecipientIdentifier.Uuid(UUID.fromString(ACI))
|
||||
: new RecipientIdentifier.Number(number));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsContactsFromOlderDaemonsWithoutTheNewRecipientListingMethod() {
|
||||
final var account = new TestAccount(NUMBER, ACI);
|
||||
account.recipients = List.of(Recipient.newBuilder().withAddress(new RecipientAddress(NUMBER)).build());
|
||||
final var signal = new DbusSignalImpl(account.manager, null, DbusConfig.getObjectPath(), true) {
|
||||
@Override
|
||||
public List<String> listRecipientIdentifiers() {
|
||||
throw new UnknownMethod("Not implemented by this daemon");
|
||||
}
|
||||
};
|
||||
final var client = new DbusManagerImpl(signal, null, "org.asamk.Signal.Test");
|
||||
assertEquals(NUMBER,
|
||||
client.getRecipients(false, Optional.empty(), Set.of(), Optional.empty())
|
||||
.getFirst()
|
||||
.getAddress()
|
||||
.number()
|
||||
.orElseThrow());
|
||||
}
|
||||
|
||||
// Run with: dbus-run-session -- ./gradlew --no-daemon :test --tests '*NumberlessDbusTest*' --rerun-tasks
|
||||
@Nested
|
||||
@Timeout(20)
|
||||
@EnabledIfEnvironmentVariable(named = "DBUS_SESSION_BUS_ADDRESS", matches = ".+")
|
||||
class SessionBus {
|
||||
|
||||
@Test
|
||||
void exportsMixedAccountsRoutesAciSelectorsAndRemovesOnlyTheClosedAccount() throws Exception {
|
||||
final var numberless = new TestAccount(null, ACI);
|
||||
final var numbered = new TestAccount(NUMBER, OTHER_ACI);
|
||||
final var secondNumberless = new TestAccount(null, THIRD_ACI);
|
||||
final var accounts = new MultiAccountManagerImpl(List.of(numberless.manager,
|
||||
numbered.manager,
|
||||
secondNumberless.manager), null);
|
||||
final var busname = newBusname();
|
||||
try (final var server = new DbusHandler(false,
|
||||
busname,
|
||||
accounts,
|
||||
true); final var connection = DBusConnectionBuilder.forSessionBus().withShared(false).build()) {
|
||||
server.init();
|
||||
final var control = connection.getRemoteObject(busname,
|
||||
DbusConfig.getObjectPath(),
|
||||
SignalControl.class);
|
||||
assertEquals(Set.of(NUMBERLESS_PATH, NUMBERED_PATH, DbusConfig.getObjectPath(THIRD_ACI)),
|
||||
control.listAccounts().stream().map(DBusPath::getPath).collect(Collectors.toSet()));
|
||||
assertEquals(NUMBERED_PATH, control.getAccount(OTHER_ACI).getPath());
|
||||
assertEquals(NUMBERED_PATH, control.getAccount(NUMBER).getPath());
|
||||
assertEquals(NUMBERLESS_PATH, control.getAccount(ACI).getPath());
|
||||
assertEquals(ACI, selectAccount(ACI, connection, busname));
|
||||
assertEquals(OTHER_ACI, selectAccount(OTHER_ACI, connection, busname));
|
||||
assertEquals(OTHER_ACI, selectAccount(NUMBER, connection, busname));
|
||||
assertThrows(UserErrorException.class, () -> selectAccount(null, connection, busname));
|
||||
|
||||
final var client = new DbusMultiAccountManagerImpl(control, connection, busname);
|
||||
assertEquals(3, client.getManagers().size());
|
||||
assertEquals(2, client.getAccountNumbers().stream().filter(n -> n == null).count());
|
||||
assertNull(client.getManager(ACI).getSelfNumber());
|
||||
assertEquals(NUMBER, client.getManager(OTHER_ACI).getSelfNumber());
|
||||
|
||||
final var removed = connection.getRemoteObject(busname, NUMBERLESS_PATH, Signal.class);
|
||||
numberless.manager.close();
|
||||
assertEquals(2, control.listAccounts().size());
|
||||
assertThrows(SignalControl.Error.Failure.class, () -> control.getAccount(ACI));
|
||||
assertThrows(DBusExecutionException.class, removed::getSelfACI);
|
||||
assertEquals(THIRD_ACI, client.getManager(THIRD_ACI).getSelfACI());
|
||||
assertEquals(NUMBER, client.getManager(NUMBER).getSelfNumber());
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
@ValueSource(strings = NUMBER)
|
||||
void selectsSingleAccountDaemonsImplicitlyAndByAci(final String number) throws Exception {
|
||||
final var account = new TestAccount(number, ACI);
|
||||
final var busname = newBusname();
|
||||
try (final var server = new DbusHandler(false,
|
||||
busname,
|
||||
account.manager,
|
||||
true); final var connection = DBusConnectionBuilder.forSessionBus().withShared(false).build()) {
|
||||
server.init();
|
||||
assertEquals(ACI, selectAccount(null, connection, busname));
|
||||
assertEquals(ACI, selectAccount(ACI, connection, busname));
|
||||
if (number != null) {
|
||||
assertEquals(ACI, selectAccount(number, connection, busname));
|
||||
}
|
||||
assertThrows(UserErrorException.class, () -> selectAccount(OTHER_ACI, connection, busname));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesAciAddressesInContactsGroupMembersAndIdentities() throws Exception {
|
||||
final var account = new TestAccount(null, ACI);
|
||||
final var contact = new RecipientAddress(UUID.fromString(OTHER_ACI));
|
||||
final var numberedContact = new RecipientAddress(NUMBER);
|
||||
account.recipients = List.of(Recipient.newBuilder().withAddress(contact).build(),
|
||||
Recipient.newBuilder().withAddress(numberedContact).build());
|
||||
account.identities = List.of(new Identity(contact,
|
||||
new byte[]{1},
|
||||
"test",
|
||||
new byte[]{2},
|
||||
TrustLevel.TRUSTED_UNVERIFIED,
|
||||
123));
|
||||
final var groupId = GroupId.unknownVersion(new byte[32]);
|
||||
account.groups = List.of(new Group(groupId,
|
||||
"Test group",
|
||||
"",
|
||||
null,
|
||||
Set.of(new GroupMember(contact, true, null, null),
|
||||
new GroupMember(numberedContact, false, null, null)),
|
||||
Set.of(contact),
|
||||
Set.of(contact),
|
||||
Set.of(contact),
|
||||
false,
|
||||
0,
|
||||
GroupPermission.EVERY_MEMBER,
|
||||
GroupPermission.EVERY_MEMBER,
|
||||
GroupPermission.EVERY_MEMBER,
|
||||
true,
|
||||
true,
|
||||
false));
|
||||
final var busname = newBusname();
|
||||
try (final var server = new DbusHandler(false,
|
||||
busname,
|
||||
account.manager,
|
||||
true); final var connection = DBusConnectionBuilder.forSessionBus().withShared(false).build()) {
|
||||
server.init();
|
||||
final var signal = connection.getRemoteObject(busname, DbusConfig.getObjectPath(), Signal.class);
|
||||
assertEquals(List.of(NUMBER), signal.listNumbers());
|
||||
assertEquals(Set.of(OTHER_ACI, NUMBER), Set.copyOf(signal.listRecipientIdentifiers()));
|
||||
try (final var client = new DbusManagerImpl(signal, connection, busname)) {
|
||||
final var contacts = client.getRecipients(false,
|
||||
Optional.empty(),
|
||||
Set.of(new RecipientIdentifier.Uuid(UUID.fromString(OTHER_ACI))),
|
||||
Optional.empty());
|
||||
assertEquals(1, contacts.size());
|
||||
assertEquals(contact, contacts.getFirst().getAddress());
|
||||
assertEquals(contact, client.getIdentities().getFirst().recipient());
|
||||
final var group = client.getGroups().getFirst();
|
||||
assertEquals(Set.of(contact, numberedContact),
|
||||
group.members().stream().map(GroupMember::recipientAddress).collect(Collectors.toSet()));
|
||||
assertEquals(Set.of(contact), group.pendingMembers());
|
||||
assertEquals(Set.of(contact), group.requestingMembers());
|
||||
assertEquals(Set.of(contact), group.bannedMembers());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendsAndReceivesNumberlessDirectGroupAndSelfMessagesOverDbus() throws Exception {
|
||||
final var account = new TestAccount(null, ACI);
|
||||
final var accounts = new MultiAccountManagerImpl(List.of(account.manager), null);
|
||||
final var busname = newBusname();
|
||||
try (final var server = new DbusHandler(false,
|
||||
busname,
|
||||
accounts,
|
||||
true); final var connection = DBusConnectionBuilder.forSessionBus().withShared(false).build()) {
|
||||
server.init();
|
||||
assertEquals(ACI, selectAccount(null, connection, busname));
|
||||
final var signal = connection.getRemoteObject(busname, NUMBERLESS_PATH, Signal.class);
|
||||
assertEquals("", signal.getSelfNumber());
|
||||
final var groupId = GroupId.unknownVersion(new byte[32]);
|
||||
assertEquals(42, signal.sendMessage("Direct", List.of(), OTHER_ACI));
|
||||
assertEquals(42, signal.sendGroupMessage("Group", List.of(), groupId.serialize()));
|
||||
assertEquals(42, signal.sendNoteToSelfMessage("Self", List.of()));
|
||||
assertEquals(Set.of(new RecipientIdentifier.Uuid(UUID.fromString(OTHER_ACI))),
|
||||
account.calls.get(0).arguments().get(1));
|
||||
assertEquals(Set.of(new RecipientIdentifier.Group(groupId)), account.calls.get(1).arguments().get(1));
|
||||
assertEquals(Set.of(RecipientIdentifier.NoteToSelf.INSTANCE), account.calls.get(2).arguments().get(1));
|
||||
assertThrows(Signal.Error.InvalidNumber.class,
|
||||
() -> signal.sendMessage("Invalid local number", List.of(), "2025550124"));
|
||||
|
||||
account.calls.clear();
|
||||
try (final var client = new DbusManagerImpl(signal, connection, busname)) {
|
||||
checkSelfOperations(client, account, new RecipientIdentifier.Uuid(UUID.fromString(ACI)));
|
||||
final var received = new LinkedBlockingQueue<MessageEnvelope>();
|
||||
client.addReceiveHandler((envelope, error) -> received.add(envelope));
|
||||
for (final var group : List.of(Optional.<GroupId>empty(), Optional.of(groupId))) {
|
||||
account.emit(messageEnvelope(group));
|
||||
final var envelope = received.poll(5, TimeUnit.SECONDS);
|
||||
assertNotNull(envelope);
|
||||
assertEquals(OTHER_ACI, envelope.sourceAddress().orElseThrow().aci().orElseThrow());
|
||||
assertEquals(Optional.empty(), envelope.sourceAddress().orElseThrow().number());
|
||||
assertEquals("Incoming", envelope.data().orElseThrow().body().orElseThrow());
|
||||
assertEquals(Optional.of(ACI),
|
||||
envelope.data().orElseThrow().mentions().getFirst().recipient().aci());
|
||||
assertEquals(group,
|
||||
envelope.data()
|
||||
.orElseThrow()
|
||||
.groupContext()
|
||||
.map(MessageEnvelope.Data.GroupContext::groupId));
|
||||
}
|
||||
}
|
||||
assertEquals(0, account.receivers.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkSelfOperations(
|
||||
final Manager client,
|
||||
final TestAccount account,
|
||||
final RecipientIdentifier.Single self
|
||||
) throws Exception {
|
||||
final Set<RecipientIdentifier> recipients = Set.of(RecipientIdentifier.NoteToSelf.INSTANCE);
|
||||
client.sendTypingMessage(TypingAction.START, recipients);
|
||||
client.sendRemoteDeleteMessage(123, recipients);
|
||||
client.sendMessageReaction("\uD83D\uDC4D", false, self, 123, recipients, false, false);
|
||||
assertEquals(List.of("sendTypingMessage", "sendRemoteDeleteMessage", "sendMessageReaction"),
|
||||
account.calls.stream().map(Call::name).toList());
|
||||
assertEquals(Set.of(self), account.calls.get(0).arguments().get(1));
|
||||
assertEquals(Set.of(self), account.calls.get(1).arguments().get(1));
|
||||
assertEquals(Set.of(self), account.calls.get(2).arguments().get(4));
|
||||
}
|
||||
|
||||
private static String selectAccount(
|
||||
final String account,
|
||||
final DBusConnection connection,
|
||||
final String busname
|
||||
) throws Exception {
|
||||
final var selected = new AtomicReference<String>();
|
||||
final var handler = new CommandHandler(null, null) {
|
||||
@Override
|
||||
public void handleLocalCommand(final LocalCommand command, final Manager manager) {
|
||||
selected.set(manager.getSelfACI());
|
||||
}
|
||||
};
|
||||
DbusCommandHandler.handleCommand(new ListGroupsCommand(), account, connection, busname, handler);
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
private static String newBusname() {
|
||||
return "org.asamk.Signal.Test" + UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
private static MessageEnvelope messageEnvelope(final Optional<GroupId> groupId) {
|
||||
final var data = new MessageEnvelope.Data(123,
|
||||
groupId.map(id -> new MessageEnvelope.Data.GroupContext(id, false, 0)),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of("Incoming"),
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
List.of(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
List.of(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
List.of(new MessageEnvelope.Data.Mention(new RecipientAddress(UUID.fromString(ACI)), 0, 1)),
|
||||
List.of(),
|
||||
List.of(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
return new MessageEnvelope(Optional.of(new RecipientAddress(UUID.fromString(OTHER_ACI))),
|
||||
1,
|
||||
123,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(data),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
private record Call(String name, List<Object> arguments) {}
|
||||
|
||||
private static final class TestAccount {
|
||||
|
||||
private final Manager manager;
|
||||
private final List<Call> calls = new CopyOnWriteArrayList<>();
|
||||
private final List<Runnable> closedListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<Manager.ReceiveMessageHandler> receivers = new CopyOnWriteArrayList<>();
|
||||
private List<Recipient> recipients = List.of();
|
||||
private List<Group> groups = List.of();
|
||||
private List<Identity> identities = List.of();
|
||||
|
||||
private TestAccount(final String number, final String aci) {
|
||||
manager = (Manager) Proxy.newProxyInstance(Manager.class.getClassLoader(),
|
||||
new Class<?>[]{Manager.class},
|
||||
(proxy, method, args) -> {
|
||||
if (method.isDefault()) {
|
||||
return InvocationHandler.invokeDefault(proxy, method, args);
|
||||
}
|
||||
return switch (method.getName()) {
|
||||
case "getSelfNumber" -> number;
|
||||
case "getSelfACI" -> aci;
|
||||
case "getLinkedDevices" -> List.of();
|
||||
case "getGroups" -> groups;
|
||||
case "getGroup" -> groups.getFirst();
|
||||
case "getIdentities" -> identities;
|
||||
case "getRecipients" -> recipients;
|
||||
case "isContactBlocked" -> false;
|
||||
case "getContactOrProfileName" -> "Test contact";
|
||||
case "addAddressChangedListener" -> null;
|
||||
case "addClosedListener" -> {
|
||||
closedListeners.add((Runnable) args[0]);
|
||||
yield null;
|
||||
}
|
||||
case "close" -> {
|
||||
closedListeners.forEach(Runnable::run);
|
||||
closedListeners.clear();
|
||||
yield null;
|
||||
}
|
||||
case "addReceiveHandler" -> {
|
||||
receivers.add((Manager.ReceiveMessageHandler) args[0]);
|
||||
yield null;
|
||||
}
|
||||
case "removeReceiveHandler" -> {
|
||||
receivers.remove(args[0]);
|
||||
yield null;
|
||||
}
|
||||
case "sendMessage", "sendTypingMessage", "sendRemoteDeleteMessage",
|
||||
"sendMessageReaction" -> {
|
||||
calls.add(new Call(method.getName(), List.of(args)));
|
||||
yield new SendMessageResults(42, Map.of());
|
||||
}
|
||||
case "hashCode" -> System.identityHashCode(proxy);
|
||||
case "equals" -> proxy == args[0];
|
||||
default -> throw new UnsupportedOperationException(method.getName());
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void emit(final MessageEnvelope envelope) {
|
||||
receivers.forEach(receiver -> receiver.handleMessage(envelope, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user