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
This commit is contained in:
John Freed 2021-10-16 18:21:09 +02:00
parent f5089789fb
commit cc738e55b5
7 changed files with 224 additions and 58 deletions

View file

@ -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<s>, members<as>, avatar<s>) -> groupId<ay>::
@ -114,17 +115,28 @@ createGroup(groupName<s>, members<as>, avatar<s>) -> groupId<ay>::
* 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<ay>) -> objectPath<o>::
* groupId : Byte array representing the internal group identifier
* objectPath : DBusPath for the group
getGroupMembers(groupId<ay>) -> members<as>::
getGroup(groupId<ay>) -> groupPath<o>::
* 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<o>::
* groupPaths : DBusPaths for the groups
All groups known are returned, regardless of their active or blocked status.
Exceptions: None
getGroupsByName(groupName<s>) -> groupIds<aay>::
* 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<s>) -> <>::
* 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<a(oays)>::
* 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<ay> (read-only) : Byte array representing the internal group identifier
* Name<s> : Display name of the group
* Description<s> : 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<aay>::
groupList : Array of Byte arrays representing the internal group identifiers
getGroupIds() -> groupIds<aay>::
* 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<ay>) -> members<as>::
* 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<ay>) -> groupName<s>::
* 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<ay>) -> isGroupBlocked<b>::
@ -274,6 +301,8 @@ isGroupBlocked(groupId<ay>) -> isGroupBlocked<b>::
Dbus will not forward messages from a group when you have blocked it.
Replacement: Use the `IsBlocked` property.
Exceptions: InvalidGroupId, Failure
isMember(groupId<ay>) -> isMember<b>::
@ -282,11 +311,17 @@ isMember(groupId<ay>) -> isMember<b>::
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<ay>) -> <>::
* 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<ay>, block<b>) -> <>::
@ -295,6 +330,8 @@ setGroupBlocked(groupId<ay>, block<b>) -> <>::
Messages from blocked groups will no longer be forwarded via DBus.
Replacement: Use the `IsBlocked` property.
Exceptions: GroupNotFound, InvalidGroupId
updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
@ -303,11 +340,19 @@ updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
* 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<s>) -> <>::
@ -315,13 +360,13 @@ addDevice(deviceUri<s>) -> <>::
getDevice(deviceId<x>) -> devicePath<o>::
* deviceId : Long representing a deviceId
* devicePath : DBusPath object for the device
* devicePath : DBusPath for the device
Exceptions: DeviceNotFound
listDevices() -> devices<a(oxs)>::
* 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<x> (read-only) : Long representing the device identifier
* Created<x> (read-only) : Long representing the number of milliseconds since the Unix epoch
* LastSeen<x> (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<s>, stop<b>) -> <>::
* recipient : Phone number of a single recipient
* targetSentTimestamp : True, if typing state should be stopped
sendTyping(number<s>, stop<b>) -> <>::
* 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<b>, groupIdStrings<as>, numbers<as>) -> <>::
* 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<s>, block<b>) -> <>::
* number : Phone number affected by method

View file

@ -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<String> groupIdStrings, List<String> numbers
) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity;
List<byte[]> getGroupsByName(String groupName);
void sendReadReceipt(
String recipient, List<Long> messageIds
) throws Error.Failure, Error.UntrustedIdentity;

View file

@ -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();

View file

@ -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<RecipientIdentifier> recipients
) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
List<String> numbers = new ArrayList<>();
List<String> 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

View file

@ -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 {

View file

@ -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<byte[]> getGroupsByName(String groupName) {
List<byte[]>groupList = new ArrayList<>();
List<byte[]>result = 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<String>(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<Long> messageIds
@ -363,6 +365,44 @@ public class DbusSignalImpl implements Signal {
}
}
@Override
public void sendTyping(String recipient, boolean stop) {
List<String> numbers = Arrays.asList(recipient);
List<String> groupIdStrings = Arrays.asList();
sendTyping(stop, null, numbers);
}
@Override
public void sendTyping(boolean stop, List<String> groupIdStrings, List<String> 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<RecipientIdentifier> recipients = new HashSet<RecipientIdentifier>();
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<String> 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("+", "_")

View file

@ -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());
}