From cc738e55b511d0d8a7ea7f81e2c4dc4c3353537c Mon Sep 17 00:00:00 2001 From: John Freed Date: Sat, 16 Oct 2021 18:21:09 +0200 Subject: [PATCH] DBus sendTyping and getGroupsByName flesh out sendTyping method to include multiple recipients new getGroupsByName to lookup groupId when you know the group name update documentation --- man/signal-cli-dbus.5.adoc | 99 ++++++++--- src/main/java/org/asamk/Signal.java | 6 + src/main/java/org/asamk/SignalControl.java | 1 + .../asamk/signal/dbus/DbusManagerImpl.java | 10 +- .../signal/dbus/DbusSignalControlImpl.java | 5 + .../org/asamk/signal/dbus/DbusSignalImpl.java | 155 ++++++++++++++---- .../org/asamk/signal/util/CommandUtil.java | 6 +- 7 files changed, 224 insertions(+), 58 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 55058580..e4641f52 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -106,6 +106,7 @@ Exceptions: None === Group control methods The following methods listen to the recipient's object path, which is constructed as follows: "/org/asamk/Signal/" + DBusNumber + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) createGroup(groupName, members, avatar) -> groupId:: @@ -114,17 +115,28 @@ createGroup(groupName, members, avatar) -> groupId:: * avatar : Filename of avatar picture to be set for group (empty if none) * groupId : Byte array representing the internal group identifier -Exceptions: AttachmentInvalid, Failure, InvalidNumber; +Exceptions: AttachmentInvalid, Failure, InvalidNumber -getGroup(groupId) -> objectPath:: -* groupId : Byte array representing the internal group identifier -* objectPath : DBusPath for the group - -getGroupMembers(groupId) -> members:: +getGroup(groupId) -> groupPath:: * groupId : Byte array representing the internal group identifier -* members : String array with the phone numbers of all active members of a group +* groupPath : DBusPath for the group -Exceptions: None, if the group name is not found an empty array is returned +Exceptions: GroupNotFound + +getGroups() -> groupPaths:: +* groupPaths : DBusPaths for the groups + +All groups known are returned, regardless of their active or blocked status. + +Exceptions: None + +getGroupsByName(groupName) -> groupIds:: +* groupName : String representing the display name of the group +* groupIds : Array of Byte arrays representing the internal group identifiers + +Returns empty array if no groups match the groupName. + +Exceptions: None joinGroup(inviteURI) -> <>:: * inviteURI : String starting with https://signal.group/# @@ -134,8 +146,8 @@ Behavior of this method depends on the `requirePermission` parameter of the `ena Exceptions: Failure listGroups() -> groups:: -* groups : Array of Structs(objectPath, groupId, groupName) -** objectPath : DBusPath +* groups : Array of Structs(groupPath, groupId, groupName) +** groupPath : DBusPath for the group ** groupId : Byte array representing the internal group identifier ** groupName : String representing the display name of the group @@ -167,10 +179,12 @@ Exceptions: Failure, GroupNotFound, InvalidGroupId === Group methods The following methods listen to the group's object path, which can be obtained from the listGroups() method and is constructed as follows: "/org/asamk/Signal/" + DBusNumber + "/Groups/" + DBusGroupId + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) * DBusGroupId : groupId in base64 format, with underscore (_) replacing plus (+), equals (=), or slash (/) Groups have the following (case-sensitive) properties: + * Id (read-only) : Byte array representing the internal group identifier * Name : Display name of the group * Description : Description of the group @@ -253,19 +267,32 @@ Exceptions: Failure === Deprecated group control methods The following deprecated methods listen to the recipient's object path, which is constructed as follows: "/org/asamk/Signal/" + DBusNumber + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) -getGroupIds() -> groupList:: -groupList : Array of Byte arrays representing the internal group identifiers +getGroupIds() -> groupIds:: +* groupIds : Array of Byte arrays representing the internal group identifiers -All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() +All groups known are returned, regardless of their active or blocked status. + +Replacement: Use the `getGroups()` method. Exceptions: None +getGroupMembers(groupId) -> members:: +* groupId : Byte array representing the internal group identifier +* members : String array with the phone numbers of all active members of a group + +Replacement: Use the `Members` property. + +Exceptions: None, if the group name is not found an empty array is returned + getGroupName(groupId) -> groupName:: * groupId : Byte array representing the internal group identifier * groupName : The display name of the group +Replacement: Use the `Name` property. + Exceptions: None, if the group name is not found an empty string is returned isGroupBlocked(groupId) -> isGroupBlocked:: @@ -274,6 +301,8 @@ isGroupBlocked(groupId) -> isGroupBlocked:: Dbus will not forward messages from a group when you have blocked it. +Replacement: Use the `IsBlocked` property. + Exceptions: InvalidGroupId, Failure isMember(groupId) -> isMember:: @@ -282,11 +311,17 @@ isMember(groupId) -> isMember:: Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) +Replacement: Use the `IsMember` property. + +Exceptions: None + quitGroup(groupId) -> <>:: * groupId : Byte array representing the internal group identifier Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember() +Replacement: Use the `quitGroup()` group method. + Exceptions: GroupNotFound, Failure, InvalidGroupId setGroupBlocked(groupId, block) -> <>:: @@ -295,6 +330,8 @@ setGroupBlocked(groupId, block) -> <>:: Messages from blocked groups will no longer be forwarded via DBus. +Replacement: Use the `IsBlocked` property. + Exceptions: GroupNotFound, InvalidGroupId updateGroup(groupId, newName, members, avatar) -> groupId:: @@ -303,11 +340,19 @@ updateGroup(groupId, newName, members, avatar) -> groupId:: * members : String array of new members to be invited to group * avatar : Filename of avatar picture to be set for group (empty if none) +To create a new group, send an empty groupId and the newName. The return value +will be the groupId randomly assigned to the new group. + +Replacement: To create a group, use the `createGroup()` group method. To +update the name, members, or avatar, use the respective property (`Name`, +`Members`, `Avatar`). + Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound === Device control methods The following methods listen to the recipient's object path, which is constructed as follows: "/org/asamk/Signal/" + DBusNumber + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) addDevice(deviceUri) -> <>:: @@ -315,13 +360,13 @@ addDevice(deviceUri) -> <>:: getDevice(deviceId) -> devicePath:: * deviceId : Long representing a deviceId -* devicePath : DBusPath object for the device +* devicePath : DBusPath for the device Exceptions: DeviceNotFound listDevices() -> devices:: -* devices : Array of structs (objectPath, id, name) -** objectPath : DBusPath representing the device's object path +* devices : Array of structs (devicePath, id, name) +** devicePath : DBusPath representing the device ** id : Long representing the deviceId ** name : String representing the device's name @@ -342,10 +387,12 @@ Exceptions: Failure === Device methods and properties The following methods listen to the device's object path, which is constructed as follows: "/org/asamk/Signal/" + DBusNumber + "/Devices/" + deviceId + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) * deviceId : Long representing the device identifier (obtained from listDevices() method) Devices have the following (case-sensitive) properties: + * Id (read-only) : Long representing the device identifier * Created (read-only) : Long representing the number of milliseconds since the Unix epoch * LastSeen (read-only) : Long representing the number of milliseconds since the Unix epoch @@ -473,11 +520,23 @@ Depending on the type of the recipient(s) field this deletes a message with one Exceptions: Failure, InvalidNumber -sendTyping(recipient, stop) -> <>:: -* recipient : Phone number of a single recipient -* targetSentTimestamp : True, if typing state should be stopped +sendTyping(number, stop) -> <>:: +* number : Phone number of a single recipient +* stop : true = stop typing, false = start typing -Exceptions: Failure, GroupNotFound, UntrustedIdentity +Stop or start sending typing indicators to a single recipient. + +Exceptions: Failure, UntrustedIdentity + +sendTyping(stop, groupIdStrings, numbers) -> <>:: +* stop : true = stop typing, false = start typing +* groupIdStrings : List of strings representing groupIds in base64-encoded format for the groups +* numbers : List of phone numbers for recipients + +Stop or start sending typing indicators to a list of recipients and/or a list of groups. The groupIdString is +derived from the groupId byte array by encoding it in base64 form. + +Exceptions: Failure, GroupNotFound, UntrustedIdentity; setContactBlocked(number, block) -> <>:: * number : Phone number affected by method diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 349671b3..eca81f18 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -31,8 +31,14 @@ public interface Signal extends DBusInterface { void sendTyping( String recipient, boolean stop + ) throws Error.Failure, Error.UntrustedIdentity; + + void sendTyping( + boolean stop, List groupIdStrings, List numbers ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity; + List getGroupsByName(String groupName); + void sendReadReceipt( String recipient, List messageIds ) throws Error.Failure, Error.UntrustedIdentity; diff --git a/src/main/java/org/asamk/SignalControl.java b/src/main/java/org/asamk/SignalControl.java index 911ccb61..4e1b77a1 100644 --- a/src/main/java/org/asamk/SignalControl.java +++ b/src/main/java/org/asamk/SignalControl.java @@ -24,6 +24,7 @@ public interface SignalControl extends DBusInterface { void verifyWithPin(String number, String verificationCode, String pin) throws Error.Failure, Error.InvalidNumber; + String link() throws Error.Failure; String link(String newDeviceName) throws Error.Failure; public String version(); diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 59422e69..156bb6d1 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -2,6 +2,7 @@ package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.signal.DbusConfig; +import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -298,14 +299,17 @@ public class DbusManagerImpl implements Manager { public void sendTypingMessage( final TypingAction action, final Set recipients ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + List numbers = new ArrayList<>(); + List groupIdStrings = new ArrayList<>(); + boolean typingAction = (action == TypingAction.START); for (final var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single) { - signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(), - action == TypingAction.STOP); + numbers.add(((RecipientIdentifier.Single) recipient).getIdentifier()); } else if (recipient instanceof RecipientIdentifier.Group) { - throw new UnsupportedOperationException(); + groupIdStrings.add(((RecipientIdentifier.Group) recipient).groupId.toBase64()); } } + signal.sendTyping(typingAction, groupIdStrings, numbers); } @Override diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index be628bde..94a0c526 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -131,6 +131,11 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { } } + @Override + public String link() throws Error.Failure { + return link("cli"); + } + @Override public String link(final String newDeviceName) throws Error.Failure { try { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ae7fc0de..0f71a932 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; +import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -23,7 +24,9 @@ import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; + import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; @@ -191,6 +194,25 @@ public class DbusSignalImpl implements Signal { } } + @Override + public List getGroupsByName(String groupName) { + ListgroupList = new ArrayList<>(); + Listresult = new ArrayList<>(); + groupList = getGroupIds(); + org.asamk.signal.manager.api.Group group = null; + for (byte[] groupId : groupList) { + try { + group = m.getGroup(getGroupId(groupId)); + } catch (AssertionError e) { + throw new Error.Failure(e.getMessage()); + } + if (group.getTitle().equals(groupName)) { + result.add(groupId); + } + } + return result; + } + @Override public long sendGroupRemoteDeleteMessage( final long targetSentTimestamp, final byte[] groupId @@ -245,26 +267,6 @@ public class DbusSignalImpl implements Signal { } } - @Override - public void sendTyping( - final String recipient, final boolean stop - ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity { - try { - var recipients = new ArrayList(1); - recipients.add(recipient); - m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START, - getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() - .map(RecipientIdentifier.class::cast) - .collect(Collectors.toSet())); - } catch (IOException e) { - throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { - throw new Error.GroupNotFound(e.getMessage()); - } catch (UntrustedIdentityException e) { - throw new Error.UntrustedIdentity(e.getMessage()); - } - } - @Override public void sendReadReceipt( final String recipient, final List messageIds @@ -363,6 +365,44 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void sendTyping(String recipient, boolean stop) { + List numbers = Arrays.asList(recipient); + List groupIdStrings = Arrays.asList(); + sendTyping(stop, null, numbers); + } + + @Override + public void sendTyping(boolean stop, List groupIdStrings, List numbers) { + final boolean noNumbers = numbers == null || numbers.isEmpty(); + final boolean noGroup = groupIdStrings == null || groupIdStrings.isEmpty(); + if (noNumbers && noGroup) { + throw new Error.Failure("No recipients given"); + } + + final TypingAction action = stop ? TypingAction.STOP : TypingAction.START; + final var timestamp = System.currentTimeMillis(); + final var localNumber = m.getSelfNumber(); + Set recipients = new HashSet(); + + try { + if (!noGroup) { + recipients.addAll(CommandUtil.getGroupIdentifiers(groupIdStrings)); + } + + if (!noNumbers) { + recipients.addAll(CommandUtil.getSingleRecipientIdentifiers(numbers, localNumber)); + } + m.sendTypingMessage(action, recipients); + } catch (IOException e) { + throw new Error.Failure("Failed to send message: " + e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage()); + } catch (Exception e) { + throw new Error.UntrustedIdentity("Failed to send message: " + e.getMessage()); + } + } + // Since contact names might be empty if not defined, also potentially return // the profile name @Override @@ -404,10 +444,19 @@ public class DbusSignalImpl implements Signal { @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) { + GroupId group = null; try { - m.setGroupBlocked(getGroupId(groupId), blocked); + group = getGroupId(groupId); + } catch (AssertionError e) { + throw new Error.Failure(e.getMessage()); + } + if (group == null) { + throw new Error.InvalidGroupId("GroupId is null."); + } + try { + m.setGroupBlocked(group, blocked); } catch (NotMasterDeviceException e) { - throw new Error.Failure("This command doesn't work on linked devices."); + throw new Error.Failure("This command doesn't work on linked device"); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException e) { @@ -443,9 +492,14 @@ public class DbusSignalImpl implements Signal { @Override public String getGroupName(final byte[] groupId) { - var group = m.getGroup(getGroupId(groupId)); - if (group == null || group.getTitle() == null) { - return ""; + org.asamk.signal.manager.api.Group group = null; + try { + group = m.getGroup(getGroupId(groupId)); + } catch (AssertionError e) { + throw new Error.Failure(e.getMessage()); + } + if (group == null) { + throw new Error.InvalidGroupId("GroupId is null."); } else { return group.getTitle(); } @@ -453,9 +507,14 @@ public class DbusSignalImpl implements Signal { @Override public List getGroupMembers(final byte[] groupId) { - var group = m.getGroup(getGroupId(groupId)); + org.asamk.signal.manager.api.Group group = null; + try { + group = m.getGroup(getGroupId(groupId)); + } catch (AssertionError e) { + throw new Error.Failure(e.getMessage()); + } if (group == null) { - return List.of(); + throw new Error.InvalidGroupId("GroupId is null."); } else { final var members = group.getMembers(); return getRecipientStrings(members); @@ -638,7 +697,15 @@ public class DbusSignalImpl implements Signal { @Override public void quitGroup(final byte[] groupId) { - var group = getGroupId(groupId); + GroupId group = null; + try { + group = getGroupId(groupId); + } catch (AssertionError e) { + throw new Error.Failure(e.getMessage()); + } + if (group == null) { + throw new Error.InvalidGroupId("GroupId is null."); + } try { m.quitGroup(group, Set.of()); } catch (GroupNotFoundException | NotAGroupMemberException e) { @@ -673,9 +740,14 @@ public class DbusSignalImpl implements Signal { @Override public boolean isGroupBlocked(final byte[] groupId) { - var group = m.getGroup(getGroupId(groupId)); + org.asamk.signal.manager.api.Group group = null; + try { + group = m.getGroup(getGroupId(groupId)); + } catch (AssertionError e) { + throw new Error.Failure(e.getMessage()); + } if (group == null) { - return false; + throw new Error.InvalidGroupId("GroupId is null."); } else { return group.isBlocked(); } @@ -683,9 +755,14 @@ public class DbusSignalImpl implements Signal { @Override public boolean isMember(final byte[] groupId) { - var group = m.getGroup(getGroupId(groupId)); + org.asamk.signal.manager.api.Group group = null; + try { + group = m.getGroup(getGroupId(groupId)); + } catch (AssertionError e) { + throw new Error.Failure(e.getMessage()); + } if (group == null) { - return false; + throw new Error.InvalidGroupId("GroupId is null."); } else { return group.isMember(); } @@ -848,6 +925,20 @@ public class DbusSignalImpl implements Signal { } private static String getGroupObjectPath(String basePath, byte[] groupId) { + /* note that DBus cannot provide a one-to-one reverse translation + * of groupPath to groupId. This is because Signal uses base64 for + * its group strings, converting any slash (/) to an underscore but + * retaining any plus (+) symbol. + * + * But DBus forbids both slash and plus, so both must be converted + * to an underscore, as DBus provides for only 63 of the 64 characters. + * + * The solution is to use the groupPath to get the Id group property, + * which is an array of bytes, then use Base64.getEncoder().encodeToString(groupId) + * to obtain the groupIdString. Note that it is theoretically possible, though + * extremely unlikely, that two different groups will map to the same groupPath. + * + */ return basePath + "/Groups/" + Base64.getEncoder() .encodeToString(groupId) .replace("+", "_") diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java index 0a624e6b..36e39c06 100644 --- a/src/main/java/org/asamk/signal/util/CommandUtil.java +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -63,12 +63,12 @@ public class CommandUtil { return groupIds; } - public static GroupId getGroupId(String groupId) throws UserErrorException { - if (groupId == null) { + public static GroupId getGroupId(String groupIdString) throws UserErrorException { + if (groupIdString == null) { return null; } try { - return GroupId.fromBase64(groupId); + return GroupId.fromBase64(groupIdString); } catch (GroupIdFormatException e) { throw new UserErrorException("Invalid group id: " + e.getMessage()); }