From d4838bd646c736a5fe1d40b45aa12be239b01ed3 Mon Sep 17 00:00:00 2001 From: John Freed Date: Thu, 7 Oct 2021 07:46:15 +0200 Subject: [PATCH 01/19] implement DBus submitRateLimitChallenge method (#763) update documentation --- man/signal-cli-dbus.5.adoc | 8 +++++++- src/main/java/org/asamk/Signal.java | 4 ++++ .../java/org/asamk/signal/dbus/DbusSignalImpl.java | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index e7cd083f..594c2941 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -363,8 +363,14 @@ uploadStickerPack(stickerPackPath) -> url:: Exception: Failure -== Signals +submitRateLimitChallenge(challenge, captcha) -> <>:: +* challenge : The challenge token taken from the proof required error. +* captcha : The captcha token from the solved captcha on the Signal website.. +Can be used to lift some rate-limits by solving a captcha. +Exception: IOErrorException + +== Signalss SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: The sync message is received when the user sends a message from a linked device. diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index b8800085..a7832714 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,5 +1,7 @@ package org.asamk; +import org.asamk.signal.commands.exceptions.IOErrorException; + import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.exceptions.DBusException; @@ -141,6 +143,8 @@ public interface Signal extends DBusInterface { String uploadStickerPack(String stickerPackPath) throws Error.Failure; + void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException; + class MessageReceived extends DBusSignal { private final long timestamp; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ab9c89b2..698ce7c1 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -1,7 +1,9 @@ package org.asamk.signal.dbus; import org.asamk.Signal; +import org.asamk.Signal.Error; import org.asamk.signal.BaseConfig; +import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -81,6 +83,18 @@ public class DbusSignalImpl implements Signal { return m.getSelfNumber(); } + @Override + public void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException { + final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", ""); + + try { + m.submitRateLimitRecaptchaChallenge(challenge, captcha); + } catch (IOException e) { + throw new IOErrorException("Submit challenge error: " + e.getMessage(), e); + } + + } + @Override public void addDevice(String uri) { try { From 7829a8d631c22a6c06f4ce176d8bccabd65f9de9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Oct 2021 17:41:31 +0200 Subject: [PATCH 02/19] Fix device id type --- src/main/java/org/asamk/Signal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index a7832714..1d8d04a3 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -262,7 +262,7 @@ public interface Signal extends DBusInterface { } } - @DBusProperty(name = "Id", type = Integer.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Id", type = Long.class, access = DBusProperty.Access.READ) @DBusProperty(name = "Name", type = String.class) @DBusProperty(name = "Created", type = String.class, access = DBusProperty.Access.READ) @DBusProperty(name = "LastSeen", type = String.class, access = DBusProperty.Access.READ) From c56a8df9b286eee786c7e81764c5394eced51295 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Oct 2021 20:51:33 +0200 Subject: [PATCH 03/19] Return struct instead of object path directly for dbus list devices --- src/main/java/org/asamk/Signal.java | 34 ++++++++- .../asamk/signal/dbus/DbusManagerImpl.java | 7 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 76 ++++++++++--------- 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 1d8d04a3..2f81c196 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -3,7 +3,9 @@ package org.asamk; import org.asamk.signal.commands.exceptions.IOErrorException; import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.annotations.DBusProperty; +import org.freedesktop.dbus.annotations.Position; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; @@ -104,7 +106,7 @@ public interface Signal extends DBusInterface { DBusPath getDevice(long deviceId); - List listDevices() throws Error.Failure; + List listDevices() throws Error.Failure; DBusPath getThisDevice(); @@ -262,6 +264,36 @@ public interface Signal extends DBusInterface { } } + class StructDevice extends Struct { + + @Position(0) + DBusPath objectPath; + + @Position(1) + Long id; + + @Position(2) + String name; + + public StructDevice(final DBusPath objectPath, final Long id, final String name) { + this.objectPath = objectPath; + this.id = id; + this.name = name; + } + + public DBusPath getObjectPath() { + return objectPath; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + @DBusProperty(name = "Id", type = Long.class, access = DBusProperty.Access.READ) @DBusProperty(name = "Name", type = String.class) @DBusProperty(name = "Created", type = String.class, access = DBusProperty.Access.READ) diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 3124a5b0..53148c01 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -145,13 +145,14 @@ public class DbusManagerImpl implements Manager { @Override public List getLinkedDevices() throws IOException { final var thisDevice = signal.getThisDevice(); - return signal.listDevices().stream().map(devicePath -> { - final var device = getRemoteObject(devicePath, Signal.Device.class).GetAll("org.asamk.Signal.Device"); + return signal.listDevices().stream().map(d -> { + final var device = getRemoteObject(d.getObjectPath(), + Signal.Device.class).GetAll("org.asamk.Signal.Device"); return new Device((long) device.get("Id").getValue(), (String) device.get("Name").getValue(), (long) device.get("Created").getValue(), (long) device.get("LastSeen").getValue(), - thisDevice.equals(devicePath)); + thisDevice.equals(d.getObjectPath())); }).collect(Collectors.toList()); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 698ce7c1..d0e33a40 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -57,7 +57,7 @@ public class DbusSignalImpl implements Signal { private final String objectPath; private DBusPath thisDevice; - private final List devices = new ArrayList<>(); + private final List devices = new ArrayList<>(); public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) { this.m = m; @@ -115,41 +115,11 @@ public class DbusSignalImpl implements Signal { } @Override - public List listDevices() { + public List listDevices() { updateDevices(); return this.devices; } - private void updateDevices() { - List linkedDevices; - try { - linkedDevices = m.getLinkedDevices(); - } catch (IOException | Error.Failure e) { - throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); - } - - unExportDevices(); - - linkedDevices.forEach(d -> { - final var object = new DbusSignalDeviceImpl(d); - final var deviceObjectPath = object.getObjectPath(); - try { - connection.exportObject(object); - } catch (DBusException e) { - e.printStackTrace(); - } - if (d.isThisDevice()) { - thisDevice = new DBusPath(deviceObjectPath); - } - this.devices.add(new DBusPath(deviceObjectPath)); - }); - } - - private void unExportDevices() { - this.devices.stream().map(DBusPath::getPath).forEach(connection::unExportObject); - this.devices.clear(); - } - @Override public DBusPath getThisDevice() { updateDevices(); @@ -802,21 +772,55 @@ public class DbusSignalImpl implements Signal { return name.isEmpty() ? null : name; } + private String emptyIfNull(final String string) { + return string == null ? "" : string; + } + private static String getDeviceObjectPath(String basePath, long deviceId) { return basePath + "/Devices/" + deviceId; } + private void updateDevices() { + List linkedDevices; + try { + linkedDevices = m.getLinkedDevices(); + } catch (IOException e) { + throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); + } + + unExportDevices(); + + linkedDevices.forEach(d -> { + final var object = new DbusSignalDeviceImpl(d); + final var deviceObjectPath = object.getObjectPath(); + try { + connection.exportObject(object); + } catch (DBusException e) { + e.printStackTrace(); + } + if (d.isThisDevice()) { + thisDevice = new DBusPath(deviceObjectPath); + } + this.devices.add(new StructDevice(new DBusPath(deviceObjectPath), d.getId(), emptyIfNull(d.getName()))); + }); + } + + private void unExportDevices() { + this.devices.stream() + .map(StructDevice::getObjectPath) + .map(DBusPath::getPath) + .forEach(connection::unExportObject); + this.devices.clear(); + } + public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device { private final org.asamk.signal.manager.api.Device device; public DbusSignalDeviceImpl(final org.asamk.signal.manager.api.Device device) { - super(); super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device", List.of(new DbusProperty<>("Id", device::getId), - new DbusProperty<>("Name", - () -> device.getName() == null ? "" : device.getName(), - this::setDeviceName), + new DbusProperty<>("Name", () -> emptyIfNull(device.getName()), this::setDeviceName), new DbusProperty<>("Created", device::getCreated), new DbusProperty<>("LastSeen", device::getLastSeen)))); this.device = device; From 179855272a9eab56bfd8514ec71e684b541feb88 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Oct 2021 20:52:10 +0200 Subject: [PATCH 04/19] Fix dbus properties GetAll method for variants --- src/main/java/org/asamk/signal/dbus/DbusProperties.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/dbus/DbusProperties.java b/src/main/java/org/asamk/signal/dbus/DbusProperties.java index 37cc35e3..bbe01d6b 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusProperties.java +++ b/src/main/java/org/asamk/signal/dbus/DbusProperties.java @@ -51,6 +51,7 @@ public abstract class DbusProperties implements Properties { } @Override + @SuppressWarnings("unchecked") public Map> GetAll(final String interface_name) { final var handler = getHandlerOptional(interface_name); if (handler.isEmpty()) { @@ -61,6 +62,9 @@ public abstract class DbusProperties implements Properties { .getProperties() .stream() .filter(p -> p.getGetter() != null) - .collect(Collectors.toMap(DbusProperty::getName, p -> new Variant<>(p.getGetter().get()))); + .collect(Collectors.toMap(DbusProperty::getName, p -> { + final Object o = p.getGetter().get(); + return o instanceof Variant ? (Variant) o : new Variant<>(o); + })); } } From cadcc6c8ef13b679d6a542b0712aad202d4a64a7 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sat, 9 Oct 2021 13:04:07 +0200 Subject: [PATCH 05/19] update docs for DBus listDevices method (#768) --- man/signal-cli-dbus.5.adoc | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 594c2941..8168c421 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -29,15 +29,18 @@ method(arg1, arg2, ...) -> return Where is according to DBus specification: -* : String -* : Byte Array -* : Array of Byte Arrays -* : String Array -* : Array of signed 64 bit integer -* : Boolean (0|1) -* : Signed 64 bit integer +* : Array of ... (comma-separated list) +* (...) : Struct (cannot be sent via `dbus-send`) +* : Boolean (false|true) (boolean:) +* : Signed 32-bit (int) integer (int32:) +* : DBusPath object (objpath:) +* : String (string:) +* : Signed 64-bit (long) integer (int64:) +* : Unsigned 8-bit (byte) integer (byte:) * <> : no return value +The final parenthetical value (such as "boolean:") is the type indicator used by `dbus-send`. + Exceptions are the names of the Java Exceptions returned in the body field. They typically contain an additional message with details. All Exceptions begin with "org.asamk.Signal.Error." which is omitted here for better readability. Phone numbers always have the format + @@ -340,8 +343,11 @@ addDevice(deviceUri) -> <>:: Exception: InvalidUri -listDevices() -> devices:: -* devices : String array of linked devices +listDevices() -> devices:: +* devices : Array of structs (objectPath, id, name) +** objectPath : DBusPath representing the device's object path +** id : Long representing the deviceId +** name : String representing the device's name Exception: Failure @@ -370,7 +376,7 @@ Can be used to lift some rate-limits by solving a captcha. Exception: IOErrorException -== Signalss +== Signals SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: The sync message is received when the user sends a message from a linked device. From b5d4a5000b1c28ca7674de5f313c876c0f29a763 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 9 Oct 2021 17:04:01 +0200 Subject: [PATCH 06/19] Add DeviceNotFound Error --- src/main/java/org/asamk/Signal.java | 7 +++++++ src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 2f81c196..bf8265ff 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -326,6 +326,13 @@ public interface Signal extends DBusInterface { } } + class DeviceNotFound extends DBusExecutionException { + + public DeviceNotFound(final String message) { + super(message); + } + } + class GroupNotFound extends DBusExecutionException { public GroupNotFound(final String message) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index d0e33a40..ab19f0ce 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -111,7 +111,11 @@ public class DbusSignalImpl implements Signal { @Override public DBusPath getDevice(long deviceId) { updateDevices(); - return new DBusPath(getDeviceObjectPath(objectPath, deviceId)); + final var deviceOptional = devices.stream().filter(g -> g.getId().equals(deviceId)).findFirst(); + if (deviceOptional.isEmpty()) { + throw new Error.DeviceNotFound("Device not found"); + } + return deviceOptional.get().getObjectPath(); } @Override From 997b4f0c3fe371efe08e92ed4678835bc62538bf Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Oct 2021 21:18:14 +0200 Subject: [PATCH 07/19] Implement new dbus group interface --- .../org/asamk/signal/manager/Manager.java | 18 +- .../org/asamk/signal/manager/ManagerImpl.java | 53 ++-- .../org/asamk/signal/manager/api/Group.java | 45 +++- .../asamk/signal/manager/api/UpdateGroup.java | 203 ++++++++++++++ .../signal/manager/helper/GroupHelper.java | 4 +- .../signal/manager/helper/SendHelper.java | 2 +- .../manager/storage/groups/GroupInfo.java | 9 +- .../manager/storage/groups/GroupInfoV1.java | 18 +- .../manager/storage/groups/GroupInfoV2.java | 38 ++- src/main/java/org/asamk/Signal.java | 89 ++++++- .../signal/commands/ListGroupsCommand.java | 16 +- .../signal/commands/UpdateGroupCommand.java | 33 +-- .../asamk/signal/dbus/DbusManagerImpl.java | 154 ++++++++--- .../org/asamk/signal/dbus/DbusProperty.java | 6 + .../org/asamk/signal/dbus/DbusSignalImpl.java | 251 ++++++++++++++++-- 15 files changed, 803 insertions(+), 136 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 7a421966..f529b408 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -8,13 +8,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; -import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -138,20 +137,7 @@ public interface Manager extends Closeable { ) throws IOException, AttachmentInvalidException; SendGroupMessageResults updateGroup( - GroupId groupId, - String name, - String description, - Set members, - Set removeMembers, - Set admins, - Set removeAdmins, - boolean resetGroupLink, - GroupLinkState groupLinkState, - GroupPermission addMemberPermission, - GroupPermission editDetailsPermission, - File avatarFile, - Integer expirationTimer, - Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException; Pair joinGroup( diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 0fd1eb33..8ccab36b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -25,13 +25,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; -import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -505,9 +504,12 @@ public class ManagerImpl implements Manager { .map(account.getRecipientStore()::resolveRecipientAddress) .collect(Collectors.toSet()), groupInfo.isBlocked(), - groupInfo.getMessageExpirationTime(), - groupInfo.isAnnouncementGroup(), - groupInfo.isMember(account.getSelfRecipientId())); + groupInfo.getMessageExpirationTimer(), + groupInfo.getPermissionAddMember(), + groupInfo.getPermissionEditDetails(), + groupInfo.getPermissionSendMessage(), + groupInfo.isMember(account.getSelfRecipientId()), + groupInfo.isAdmin(account.getSelfRecipientId())); } @Override @@ -532,35 +534,22 @@ public class ManagerImpl implements Manager { @Override public SendGroupMessageResults updateGroup( - GroupId groupId, - String name, - String description, - Set members, - Set removeMembers, - Set admins, - Set removeAdmins, - boolean resetGroupLink, - GroupLinkState groupLinkState, - GroupPermission addMemberPermission, - GroupPermission editDetailsPermission, - File avatarFile, - Integer expirationTimer, - Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { return groupHelper.updateGroup(groupId, - name, - description, - members == null ? null : resolveRecipients(members), - removeMembers == null ? null : resolveRecipients(removeMembers), - admins == null ? null : resolveRecipients(admins), - removeAdmins == null ? null : resolveRecipients(removeAdmins), - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); + updateGroup.getName(), + updateGroup.getDescription(), + updateGroup.getMembers() == null ? null : resolveRecipients(updateGroup.getMembers()), + updateGroup.getRemoveMembers() == null ? null : resolveRecipients(updateGroup.getRemoveMembers()), + updateGroup.getAdmins() == null ? null : resolveRecipients(updateGroup.getAdmins()), + updateGroup.getRemoveAdmins() == null ? null : resolveRecipients(updateGroup.getRemoveAdmins()), + updateGroup.isResetGroupLink(), + updateGroup.getGroupLinkState(), + updateGroup.getAddMemberPermission(), + updateGroup.getEditDetailsPermission(), + updateGroup.getAvatarFile(), + updateGroup.getExpirationTimer(), + updateGroup.getIsAnnouncementGroup()); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Group.java b/lib/src/main/java/org/asamk/signal/manager/api/Group.java index 650e10b6..4787ef95 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Group.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Group.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.api; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import java.util.Set; @@ -17,9 +18,13 @@ public class Group { private final Set requestingMembers; private final Set adminMembers; private final boolean isBlocked; - private final int messageExpirationTime; - private final boolean isAnnouncementGroup; + private final int messageExpirationTimer; + + private final GroupPermission permissionAddMember; + private final GroupPermission permissionEditDetails; + private final GroupPermission permissionSendMessage; private final boolean isMember; + private final boolean isAdmin; public Group( final GroupId groupId, @@ -31,9 +36,12 @@ public class Group { final Set requestingMembers, final Set adminMembers, final boolean isBlocked, - final int messageExpirationTime, - final boolean isAnnouncementGroup, - final boolean isMember + final int messageExpirationTimer, + final GroupPermission permissionAddMember, + final GroupPermission permissionEditDetails, + final GroupPermission permissionSendMessage, + final boolean isMember, + final boolean isAdmin ) { this.groupId = groupId; this.title = title; @@ -44,9 +52,12 @@ public class Group { this.requestingMembers = requestingMembers; this.adminMembers = adminMembers; this.isBlocked = isBlocked; - this.messageExpirationTime = messageExpirationTime; - this.isAnnouncementGroup = isAnnouncementGroup; + this.messageExpirationTimer = messageExpirationTimer; + this.permissionAddMember = permissionAddMember; + this.permissionEditDetails = permissionEditDetails; + this.permissionSendMessage = permissionSendMessage; this.isMember = isMember; + this.isAdmin = isAdmin; } public GroupId getGroupId() { @@ -85,15 +96,27 @@ public class Group { return isBlocked; } - public int getMessageExpirationTime() { - return messageExpirationTime; + public int getMessageExpirationTimer() { + return messageExpirationTimer; } - public boolean isAnnouncementGroup() { - return isAnnouncementGroup; + public GroupPermission getPermissionAddMember() { + return permissionAddMember; + } + + public GroupPermission getPermissionEditDetails() { + return permissionEditDetails; + } + + public GroupPermission getPermissionSendMessage() { + return permissionSendMessage; } public boolean isMember() { return isMember; } + + public boolean isAdmin() { + return isAdmin; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java b/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java new file mode 100644 index 00000000..b5877ae5 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java @@ -0,0 +1,203 @@ +package org.asamk.signal.manager.api; + +import org.asamk.signal.manager.groups.GroupLinkState; +import org.asamk.signal.manager.groups.GroupPermission; + +import java.io.File; +import java.util.Set; + +public class UpdateGroup { + + private final String name; + private final String description; + private final Set members; + private final Set removeMembers; + private final Set admins; + private final Set removeAdmins; + private final boolean resetGroupLink; + private final GroupLinkState groupLinkState; + private final GroupPermission addMemberPermission; + private final GroupPermission editDetailsPermission; + private final File avatarFile; + private final Integer expirationTimer; + private final Boolean isAnnouncementGroup; + + private UpdateGroup(final Builder builder) { + name = builder.name; + description = builder.description; + members = builder.members; + removeMembers = builder.removeMembers; + admins = builder.admins; + removeAdmins = builder.removeAdmins; + resetGroupLink = builder.resetGroupLink; + groupLinkState = builder.groupLinkState; + addMemberPermission = builder.addMemberPermission; + editDetailsPermission = builder.editDetailsPermission; + avatarFile = builder.avatarFile; + expirationTimer = builder.expirationTimer; + isAnnouncementGroup = builder.isAnnouncementGroup; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(final UpdateGroup copy) { + Builder builder = new Builder(); + builder.name = copy.getName(); + builder.description = copy.getDescription(); + builder.members = copy.getMembers(); + builder.removeMembers = copy.getRemoveMembers(); + builder.admins = copy.getAdmins(); + builder.removeAdmins = copy.getRemoveAdmins(); + builder.resetGroupLink = copy.isResetGroupLink(); + builder.groupLinkState = copy.getGroupLinkState(); + builder.addMemberPermission = copy.getAddMemberPermission(); + builder.editDetailsPermission = copy.getEditDetailsPermission(); + builder.avatarFile = copy.getAvatarFile(); + builder.expirationTimer = copy.getExpirationTimer(); + builder.isAnnouncementGroup = copy.getIsAnnouncementGroup(); + return builder; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Set getMembers() { + return members; + } + + public Set getRemoveMembers() { + return removeMembers; + } + + public Set getAdmins() { + return admins; + } + + public Set getRemoveAdmins() { + return removeAdmins; + } + + public boolean isResetGroupLink() { + return resetGroupLink; + } + + public GroupLinkState getGroupLinkState() { + return groupLinkState; + } + + public GroupPermission getAddMemberPermission() { + return addMemberPermission; + } + + public GroupPermission getEditDetailsPermission() { + return editDetailsPermission; + } + + public File getAvatarFile() { + return avatarFile; + } + + public Integer getExpirationTimer() { + return expirationTimer; + } + + public Boolean getIsAnnouncementGroup() { + return isAnnouncementGroup; + } + + public static final class Builder { + + private String name; + private String description; + private Set members; + private Set removeMembers; + private Set admins; + private Set removeAdmins; + private boolean resetGroupLink; + private GroupLinkState groupLinkState; + private GroupPermission addMemberPermission; + private GroupPermission editDetailsPermission; + private File avatarFile; + private Integer expirationTimer; + private Boolean isAnnouncementGroup; + + private Builder() { + } + + public Builder withName(final String val) { + name = val; + return this; + } + + public Builder withDescription(final String val) { + description = val; + return this; + } + + public Builder withMembers(final Set val) { + members = val; + return this; + } + + public Builder withRemoveMembers(final Set val) { + removeMembers = val; + return this; + } + + public Builder withAdmins(final Set val) { + admins = val; + return this; + } + + public Builder withRemoveAdmins(final Set val) { + removeAdmins = val; + return this; + } + + public Builder withResetGroupLink(final boolean val) { + resetGroupLink = val; + return this; + } + + public Builder withGroupLinkState(final GroupLinkState val) { + groupLinkState = val; + return this; + } + + public Builder withAddMemberPermission(final GroupPermission val) { + addMemberPermission = val; + return this; + } + + public Builder withEditDetailsPermission(final GroupPermission val) { + editDetailsPermission = val; + return this; + } + + public Builder withAvatarFile(final File val) { + avatarFile = val; + return this; + } + + public Builder withExpirationTimer(final Integer val) { + expirationTimer = val; + return this; + } + + public Builder withIsAnnouncementGroup(final Boolean val) { + isAnnouncementGroup = val; + return this; + } + + public UpdateGroup build() { + return new UpdateGroup(this); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 62f4f111..ee2e9416 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -639,7 +639,7 @@ public class GroupHelper { return SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + .withExpiration(g.getMessageExpirationTimer()); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { @@ -648,7 +648,7 @@ public class GroupHelper { .withSignedGroupChange(signedGroupChange); return SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + .withExpiration(g.getMessageExpirationTimer()); } private SendGroupMessageResults sendUpdateGroupV2Message( diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index c0953f1f..6c0fb2e9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -100,7 +100,7 @@ public class SendHelper { final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g ) throws IOException, GroupSendingNotAllowedException { GroupUtils.setGroupContext(messageBuilder, g); - messageBuilder.withExpiration(g.getMessageExpirationTime()); + messageBuilder.withExpiration(g.getMessageExpirationTimer()); final var message = messageBuilder.build(); final var recipients = g.getMembersWithout(account.getSelfRecipientId()); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index 60efc84b..b4f4e63a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.storage.recipients.RecipientId; import java.util.Set; @@ -38,10 +39,16 @@ public abstract class GroupInfo { public abstract void setBlocked(boolean blocked); - public abstract int getMessageExpirationTime(); + public abstract int getMessageExpirationTimer(); public abstract boolean isAnnouncementGroup(); + public abstract GroupPermission getPermissionAddMember(); + + public abstract GroupPermission getPermissionEditDetails(); + + public abstract GroupPermission getPermissionSendMessage(); + public Set getMembersWithout(RecipientId recipientId) { return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index 49c9a504..dbd2dcbb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.storage.groups; import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.storage.recipients.RecipientId; @@ -85,7 +86,7 @@ public class GroupInfoV1 extends GroupInfo { } @Override - public int getMessageExpirationTime() { + public int getMessageExpirationTimer() { return messageExpirationTime; } @@ -94,6 +95,21 @@ public class GroupInfoV1 extends GroupInfo { return false; } + @Override + public GroupPermission getPermissionAddMember() { + return GroupPermission.EVERY_MEMBER; + } + + @Override + public GroupPermission getPermissionEditDetails() { + return GroupPermission.EVERY_MEMBER; + } + + @Override + public GroupPermission getPermissionSendMessage() { + return GroupPermission.EVERY_MEMBER; + } + public void addMembers(Collection members) { this.members.addAll(members); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index a06b83df..34db2a28 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.signal.storageservice.protos.groups.AccessControl; @@ -151,7 +152,7 @@ public class GroupInfoV2 extends GroupInfo { } @Override - public int getMessageExpirationTime() { + public int getMessageExpirationTimer() { return this.group != null && this.group.hasDisappearingMessagesTimer() ? this.group.getDisappearingMessagesTimer().getDuration() : 0; @@ -162,6 +163,23 @@ public class GroupInfoV2 extends GroupInfo { return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED; } + @Override + public GroupPermission getPermissionAddMember() { + final var accessControl = getAccessControl(); + return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getMembers()); + } + + @Override + public GroupPermission getPermissionEditDetails() { + final var accessControl = getAccessControl(); + return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getAttributes()); + } + + @Override + public GroupPermission getPermissionSendMessage() { + return isAnnouncementGroup() ? GroupPermission.ONLY_ADMINS : GroupPermission.EVERY_MEMBER; + } + public void setPermissionDenied(final boolean permissionDenied) { this.permissionDenied = permissionDenied; } @@ -169,4 +187,22 @@ public class GroupInfoV2 extends GroupInfo { public boolean isPermissionDenied() { return permissionDenied; } + + private AccessControl getAccessControl() { + if (this.group == null || !this.group.hasAccessControl()) { + return null; + } + + return this.group.getAccessControl(); + } + + private static GroupPermission toGroupPermission(final AccessControl.AccessRequired permission) { + switch (permission) { + case ADMINISTRATOR: + return GroupPermission.ONLY_ADMINS; + case MEMBER: + default: + return GroupPermission.EVERY_MEMBER; + } + } } diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index bf8265ff..349671b3 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,7 +1,6 @@ package org.asamk; import org.asamk.signal.commands.exceptions.IOErrorException; - import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.annotations.DBusProperty; @@ -84,14 +83,27 @@ public interface Signal extends DBusInterface { void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; + @Deprecated void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId; + @Deprecated List getGroupIds(); + DBusPath getGroup(byte[] groupId); + + List listGroups(); + + @Deprecated String getGroupName(byte[] groupId) throws Error.InvalidGroupId; + @Deprecated List getGroupMembers(byte[] groupId) throws Error.InvalidGroupId; + byte[] createGroup( + String name, List members, String avatar + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber; + + @Deprecated byte[] updateGroup( byte[] groupId, String name, List members, String avatar ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId; @@ -133,12 +145,15 @@ public interface Signal extends DBusInterface { List getContactNumber(final String name) throws Error.Failure; + @Deprecated void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId; boolean isContactBlocked(final String number) throws Error.InvalidNumber; + @Deprecated boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId; + @Deprecated boolean isMember(final byte[] groupId) throws Error.InvalidGroupId; byte[] joinGroup(final String groupLink) throws Error.Failure; @@ -303,6 +318,71 @@ public interface Signal extends DBusInterface { void removeDevice() throws Error.Failure; } + class StructGroup extends Struct { + + @Position(0) + DBusPath objectPath; + + @Position(1) + byte[] id; + + @Position(2) + String name; + + public StructGroup(final DBusPath objectPath, final byte[] id, final String name) { + this.objectPath = objectPath; + this.id = id; + this.name = name; + } + + public DBusPath getObjectPath() { + return objectPath; + } + + public byte[] getId() { + return id; + } + + public String getName() { + return name; + } + } + + @DBusProperty(name = "Id", type = Byte[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Name", type = String.class) + @DBusProperty(name = "Description", type = String.class) + @DBusProperty(name = "Avatar", type = String.class, access = DBusProperty.Access.WRITE) + @DBusProperty(name = "IsBlocked", type = Boolean.class) + @DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "MessageExpirationTimer", type = Integer.class) + @DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "RequestingMembers", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Admins", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "PermissionAddMember", type = String.class) + @DBusProperty(name = "PermissionEditDetails", type = String.class) + @DBusProperty(name = "PermissionSendMessage", type = String.class) + @DBusProperty(name = "GroupInviteLink", type = String.class, access = DBusProperty.Access.READ) + interface Group extends DBusInterface, Properties { + + void quitGroup() throws Error.Failure, Error.LastGroupAdmin; + + void addMembers(List recipients) throws Error.Failure; + + void removeMembers(List recipients) throws Error.Failure; + + void addAdmins(List recipients) throws Error.Failure; + + void removeAdmins(List recipients) throws Error.Failure; + + void resetLink() throws Error.Failure; + + void disableLink() throws Error.Failure; + + void enableLink(boolean requiresApproval) throws Error.Failure; + } + interface Error { class AttachmentInvalid extends DBusExecutionException { @@ -347,6 +427,13 @@ public interface Signal extends DBusInterface { } } + class LastGroupAdmin extends DBusExecutionException { + + public LastGroupAdmin(final String message) { + super(message); + } + } + class InvalidNumber extends DBusExecutionException { public InvalidNumber(final String message) { diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index fd8c4b92..b2182429 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -63,7 +63,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { resolveMembers(group.getPendingMembers()), resolveMembers(group.getRequestingMembers()), resolveMembers(group.getAdminMembers()), - group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s", + group.getMessageExpirationTimer() == 0 ? "disabled" : group.getMessageExpirationTimer() + "s", groupInviteLink == null ? '-' : groupInviteLink.getUrl()); } else { writer.println("Id: {} Name: {} Active: {} Blocked: {}", @@ -91,11 +91,14 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { group.getDescription(), group.isMember(), group.isBlocked(), - group.getMessageExpirationTime(), + group.getMessageExpirationTimer(), resolveJsonMembers(group.getMembers()), resolveJsonMembers(group.getPendingMembers()), resolveJsonMembers(group.getRequestingMembers()), resolveJsonMembers(group.getAdminMembers()), + group.getPermissionAddMember().name(), + group.getPermissionEditDetails().name(), + group.getPermissionSendMessage().name(), groupInviteLink == null ? null : groupInviteLink.getUrl()); }).collect(Collectors.toList()); @@ -122,6 +125,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { public final Set pendingMembers; public final Set requestingMembers; public final Set admins; + public final String permissionAddMember; + public final String permissionEditDetails; + public final String permissionSendMessage; public final String groupInviteLink; public JsonGroup( @@ -135,6 +141,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { Set pendingMembers, Set requestingMembers, Set admins, + final String permissionAddMember, + final String permissionEditDetails, + final String permissionSendMessage, String groupInviteLink ) { this.id = id; @@ -148,6 +157,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { this.pendingMembers = pendingMembers; this.requestingMembers = requestingMembers; this.admins = admins; + this.permissionAddMember = permissionAddMember; + this.permissionEditDetails = permissionEditDetails; + this.permissionSendMessage = permissionSendMessage; this.groupInviteLink = groupInviteLink; } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 68bce2d2..b63a7160 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -12,6 +12,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; @@ -145,21 +146,23 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { } var results = m.updateGroup(groupId, - groupName, - groupDescription, - groupMembers, - groupRemoveMembers, - groupAdmins, - groupRemoveAdmins, - groupResetLink, - groupLinkState, - groupAddMemberPermission, - groupEditDetailsPermission, - groupAvatar == null ? null : new File(groupAvatar), - groupExpiration, - groupSendMessagesPermission == null - ? null - : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS); + UpdateGroup.newBuilder() + .withName(groupName) + .withDescription(groupDescription) + .withMembers(groupMembers) + .withRemoveMembers(groupRemoveMembers) + .withAdmins(groupAdmins) + .withRemoveAdmins(groupRemoveAdmins) + .withResetGroupLink(groupResetLink) + .withGroupLinkState(groupLinkState) + .withAddMemberPermission(groupAddMemberPermission) + .withEditDetailsPermission(groupEditDetailsPermission) + .withAvatarFile(groupAvatar == null ? null : new File(groupAvatar)) + .withExpirationTimer(groupExpiration) + .withIsAnnouncementGroup(groupSendMessagesPermission == null + ? null + : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS) + .build()); if (results != null) { timestamp = results.getTimestamp(); ErrorUtils.handleSendMessageResults(results.getResults()); diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 53148c01..6e655bdc 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -15,9 +15,9 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; -import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; @@ -183,8 +183,8 @@ public class DbusManagerImpl implements Manager { @Override public List getGroups() { - final var groupIds = signal.getGroupIds(); - return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList()); + final var groups = signal.listGroups(); + return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList()); } @Override @@ -194,7 +194,8 @@ public class DbusManagerImpl implements Manager { if (groupAdmins.size() > 0) { throw new UnsupportedOperationException(); } - signal.quitGroup(groupId.serialize()); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.quitGroup(); return new SendGroupMessageResults(0, List.of()); } @@ -207,8 +208,7 @@ public class DbusManagerImpl implements Manager { public Pair createGroup( final String name, final Set members, final File avatarFile ) throws IOException, AttachmentInvalidException { - final var newGroupId = signal.updateGroup(new byte[0], - emptyIfNull(name), + final var newGroupId = signal.createGroup(emptyIfNull(name), members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), avatarFile == null ? "" : avatarFile.getPath()); return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); @@ -216,25 +216,76 @@ public class DbusManagerImpl implements Manager { @Override public SendGroupMessageResults updateGroup( - final GroupId groupId, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { - signal.updateGroup(groupId.serialize(), - emptyIfNull(name), - members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), - avatarFile == null ? "" : avatarFile.getPath()); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + if (updateGroup.getName() != null) { + group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName()); + } + if (updateGroup.getDescription() != null) { + group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription()); + } + if (updateGroup.getAvatarFile() != null) { + group.Set("org.asamk.Signal.Group", + "Avatar", + updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath()); + } + if (updateGroup.getExpirationTimer() != null) { + group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer()); + } + if (updateGroup.getAddMemberPermission() != null) { + group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name()); + } + if (updateGroup.getEditDetailsPermission() != null) { + group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name()); + } + if (updateGroup.getIsAnnouncementGroup() != null) { + group.Set("org.asamk.Signal.Group", + "PermissionSendMessage", + updateGroup.getIsAnnouncementGroup() + ? GroupPermission.ONLY_ADMINS.name() + : GroupPermission.EVERY_MEMBER.name()); + } + if (updateGroup.getMembers() != null) { + group.addMembers(updateGroup.getMembers() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getRemoveMembers() != null) { + group.removeMembers(updateGroup.getRemoveMembers() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getAdmins() != null) { + group.addAdmins(updateGroup.getAdmins() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getRemoveAdmins() != null) { + group.removeAdmins(updateGroup.getRemoveAdmins() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.isResetGroupLink()) { + group.resetLink(); + } + if (updateGroup.getGroupLinkState() != null) { + switch (updateGroup.getGroupLinkState()) { + case DISABLED: + group.disableLink(); + break; + case ENABLED: + group.enableLink(false); + break; + case ENABLED_WITH_APPROVAL: + group.enableLink(true); + break; + } + } return new SendGroupMessageResults(0, List.of()); } @@ -344,7 +395,12 @@ public class DbusManagerImpl implements Manager { public void setGroupBlocked( final GroupId groupId, final boolean blocked ) throws GroupNotFoundException, IOException { - signal.setGroupBlocked(groupId.serialize(), blocked); + setGroupProperty(groupId, "IsBlocked", blocked); + } + + private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) { + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.Set("org.asamk.Signal.Group", propertyName, blocked); } @Override @@ -411,19 +467,41 @@ public class DbusManagerImpl implements Manager { @Override public Group getGroup(final GroupId groupId) { - final var id = groupId.serialize(); - return new Group(groupId, - signal.getGroupName(id), - null, - null, - signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()), - Set.of(), - Set.of(), - Set.of(), - signal.isGroupBlocked(id), - 0, - false, - signal.isMember(id)); + final var groupPath = signal.getGroup(groupId.serialize()); + return getGroup(groupPath); + } + + @SuppressWarnings("unchecked") + private Group getGroup(final DBusPath groupPath) { + final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group"); + final var id = (byte[]) group.get("Id").getValue(); + try { + return new Group(GroupId.unknownVersion(id), + (String) group.get("Name").getValue(), + (String) group.get("Description").getValue(), + GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()), + ((List) group.get("Members").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("PendingMembers").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("RequestingMembers").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("Admins").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + (boolean) group.get("IsBlocked").getValue(), + (int) group.get("MessageExpirationTimer").getValue(), + GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()), + GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()), + GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()), + (boolean) group.get("IsMember").getValue(), + (boolean) group.get("IsAdmin").getValue()); + } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { + throw new AssertionError(e); + } } @Override diff --git a/src/main/java/org/asamk/signal/dbus/DbusProperty.java b/src/main/java/org/asamk/signal/dbus/DbusProperty.java index e0557786..5042458e 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusProperty.java +++ b/src/main/java/org/asamk/signal/dbus/DbusProperty.java @@ -21,6 +21,12 @@ public class DbusProperty { this.setter = null; } + public DbusProperty(final String name, final Consumer setter) { + this.name = name; + this.getter = null; + this.setter = setter; + } + public String getName() { return name; } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ab19f0ce..56ecdc55 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -1,7 +1,6 @@ package org.asamk.signal.dbus; import org.asamk.Signal; -import org.asamk.Signal.Error; import org.asamk.signal.BaseConfig; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.AttachmentInvalidException; @@ -13,9 +12,12 @@ import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -26,6 +28,7 @@ import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.freedesktop.dbus.types.Variant; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -40,6 +43,8 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -58,6 +63,7 @@ public class DbusSignalImpl implements Signal { private DBusPath thisDevice; private final List devices = new ArrayList<>(); + private final List groups = new ArrayList<>(); public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) { this.m = m; @@ -67,6 +73,7 @@ public class DbusSignalImpl implements Signal { public void initObjects() { updateDevices(); + updateGroups(); } public void close() { @@ -415,6 +422,22 @@ public class DbusSignalImpl implements Signal { return ids; } + @Override + public DBusPath getGroup(final byte[] groupId) { + updateGroups(); + final var groupOptional = groups.stream().filter(g -> Arrays.equals(g.getId(), groupId)).findFirst(); + if (groupOptional.isEmpty()) { + throw new Error.GroupNotFound("Group not found"); + } + return groupOptional.get().getObjectPath(); + } + + @Override + public List listGroups() { + updateGroups(); + return groups; + } + @Override public String getGroupName(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); @@ -431,10 +454,18 @@ public class DbusSignalImpl implements Signal { if (group == null) { return List.of(); } else { - return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); + final var members = group.getMembers(); + return getRecipientStrings(members); } } + @Override + public byte[] createGroup( + final String name, final List members, final String avatar + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber { + return updateGroup(new byte[0], name, members, avatar); + } + @Override public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) { try { @@ -448,19 +479,11 @@ public class DbusSignalImpl implements Signal { return results.first().serialize(); } else { final var results = m.updateGroup(getGroupId(groupId), - name, - null, - memberIdentifiers, - null, - null, - null, - false, - null, - null, - null, - avatar == null ? null : new File(avatar), - null, - null); + UpdateGroup.newBuilder() + .withName(name) + .withMembers(memberIdentifiers) + .withAvatarFile(avatar == null ? null : new File(avatar)) + .build()); if (results != null) { checkSendMessageResults(results.getTimestamp(), results.getResults()); } @@ -740,6 +763,10 @@ public class DbusSignalImpl implements Signal { throw new Error.Failure(message.toString()); } + private static List getRecipientStrings(final Set members) { + return members.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); + } + private static Set getSingleRecipientIdentifiers( final Collection recipientStrings, final String localNumber ) throws DBusExecutionException { @@ -817,6 +844,38 @@ public class DbusSignalImpl implements Signal { this.devices.clear(); } + private static String getGroupObjectPath(String basePath, byte[] groupId) { + return basePath + "/Groups/" + Base64.getEncoder() + .encodeToString(groupId) + .replace("+", "_") + .replace("/", "_") + .replace("=", "_"); + } + + private void updateGroups() { + List groups; + groups = m.getGroups(); + + unExportGroups(); + + groups.forEach(g -> { + final var object = new DbusSignalGroupImpl(g.getGroupId()); + try { + connection.exportObject(object); + } catch (DBusException e) { + e.printStackTrace(); + } + this.groups.add(new StructGroup(new DBusPath(object.getObjectPath()), + g.getGroupId().serialize(), + emptyIfNull(g.getTitle()))); + }); + } + + private void unExportGroups() { + this.groups.stream().map(StructGroup::getObjectPath).map(DBusPath::getPath).forEach(connection::unExportObject); + this.groups.clear(); + } + public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device { private final org.asamk.signal.manager.api.Device device; @@ -858,4 +917,166 @@ public class DbusSignalImpl implements Signal { } } } + + public class DbusSignalGroupImpl extends DbusProperties implements Signal.Group { + + private final GroupId groupId; + + public DbusSignalGroupImpl(final GroupId groupId) { + this.groupId = groupId; + super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group", + List.of(new DbusProperty<>("Id", groupId::serialize), + new DbusProperty<>("Name", () -> emptyIfNull(getGroup().getTitle()), this::setGroupName), + new DbusProperty<>("Description", + () -> emptyIfNull(getGroup().getDescription()), + this::setGroupDescription), + new DbusProperty<>("Avatar", this::setGroupAvatar), + new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked), + new DbusProperty<>("IsMember", () -> getGroup().isMember()), + new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()), + new DbusProperty<>("MessageExpirationTimer", + () -> getGroup().getMessageExpirationTimer(), + this::setMessageExpirationTime), + new DbusProperty<>("Members", + () -> new Variant<>(getRecipientStrings(getGroup().getMembers()), "as")), + new DbusProperty<>("PendingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().getPendingMembers()), "as")), + new DbusProperty<>("RequestingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().getRequestingMembers()), "as")), + new DbusProperty<>("Admins", + () -> new Variant<>(getRecipientStrings(getGroup().getAdminMembers()), "as")), + new DbusProperty<>("PermissionAddMember", + () -> getGroup().getPermissionAddMember().name(), + this::setGroupPermissionAddMember), + new DbusProperty<>("PermissionEditDetails", + () -> getGroup().getPermissionEditDetails().name(), + this::setGroupPermissionEditDetails), + new DbusProperty<>("PermissionSendMessage", + () -> getGroup().getPermissionSendMessage().name(), + this::setGroupPermissionSendMessage), + new DbusProperty<>("GroupInviteLink", () -> { + final var groupInviteLinkUrl = getGroup().getGroupInviteLinkUrl(); + return groupInviteLinkUrl == null ? "" : groupInviteLinkUrl.getUrl(); + })))); + } + + @Override + public String getObjectPath() { + return getGroupObjectPath(objectPath, groupId.serialize()); + } + + @Override + public void quitGroup() throws Error.Failure { + try { + m.quitGroup(groupId, Set.of()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (LastGroupAdminException e) { + throw new Error.LastGroupAdmin(e.getMessage()); + } + } + + @Override + public void addMembers(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withMembers(memberIdentifiers).build()); + } + + @Override + public void removeMembers(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withRemoveMembers(memberIdentifiers).build()); + } + + @Override + public void addAdmins(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withAdmins(memberIdentifiers).build()); + } + + @Override + public void removeAdmins(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withRemoveAdmins(memberIdentifiers).build()); + } + + @Override + public void resetLink() throws Error.Failure { + updateGroup(UpdateGroup.newBuilder().withResetGroupLink(true).build()); + } + + @Override + public void disableLink() throws Error.Failure { + updateGroup(UpdateGroup.newBuilder().withGroupLinkState(GroupLinkState.DISABLED).build()); + } + + @Override + public void enableLink(final boolean requiresApproval) throws Error.Failure { + updateGroup(UpdateGroup.newBuilder() + .withGroupLinkState(requiresApproval + ? GroupLinkState.ENABLED_WITH_APPROVAL + : GroupLinkState.ENABLED) + .build()); + } + + private org.asamk.signal.manager.api.Group getGroup() { + return m.getGroup(groupId); + } + + private void setGroupName(final String name) { + updateGroup(UpdateGroup.newBuilder().withName(name).build()); + } + + private void setGroupDescription(final String description) { + updateGroup(UpdateGroup.newBuilder().withDescription(description).build()); + } + + private void setGroupAvatar(final String avatar) { + updateGroup(UpdateGroup.newBuilder().withAvatarFile(new File(avatar)).build()); + } + + private void setMessageExpirationTime(final int expirationTime) { + updateGroup(UpdateGroup.newBuilder().withExpirationTimer(expirationTime).build()); + } + + private void setGroupPermissionAddMember(final String permission) { + updateGroup(UpdateGroup.newBuilder().withAddMemberPermission(GroupPermission.valueOf(permission)).build()); + } + + private void setGroupPermissionEditDetails(final String permission) { + updateGroup(UpdateGroup.newBuilder() + .withEditDetailsPermission(GroupPermission.valueOf(permission)) + .build()); + } + + private void setGroupPermissionSendMessage(final String permission) { + updateGroup(UpdateGroup.newBuilder() + .withIsAnnouncementGroup(GroupPermission.valueOf(permission) == GroupPermission.ONLY_ADMINS) + .build()); + } + + private void setIsBlocked(final boolean isBlocked) { + try { + m.setGroupBlocked(groupId, isBlocked); + } catch (GroupNotFoundException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + private void updateGroup(final UpdateGroup updateGroup) { + try { + m.updateGroup(groupId, updateGroup); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } + } + } } From 6501ffcdacc2ada63f30a00299d437d5aeff1293 Mon Sep 17 00:00:00 2001 From: John Freed Date: Tue, 21 Sep 2021 16:42:51 +0200 Subject: [PATCH 08/19] Update documentation --- man/signal-cli-dbus.5.adoc | 148 ++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 59 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 8168c421..3d977de6 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -48,9 +48,9 @@ Phone numbers always have the format + == Methods === Control methods -These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`). -Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to -`/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_). +These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`). +Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to +`/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_). Only `version()` is activated in single-user mode; the rest are disabled. link() -> deviceLinkUri:: @@ -63,7 +63,7 @@ can be captured by a Signal smartphone client. 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` -Exception: Failure +Exceptions: Failure listAccounts() -> accountList:: * accountList : Array of all attached accounts in DBus object path form @@ -89,20 +89,28 @@ verify(number, verificationCode) -> <>:: Command fails if PIN was set after previous registration; use verifyWithPin instead. -Exception: Failure, InvalidNumber +Exceptions: Failure, InvalidNumber verifyWithPin(number, verificationCode, pin) -> <>:: * number : Phone number * verificationCode : Code received from Signal after successful registration request * pin : PIN you set with setPin command after verifying previous registration -Exception: Failure, InvalidNumber +Exceptions: Failure, InvalidNumber version() -> version:: * version : Version string of signal-cli Exceptions: None +=== Device methods +Requests for these methods are sent to a specific device (main or linked); the list is available +from the listDevices() method (see below under "Other methods"). + +removeDevice() -> <>:: + +Exceptions: Failure + === Other methods updateGroup(groupId, newName, members, avatar) -> groupId:: @@ -130,11 +138,11 @@ setExpirationTimer(number, expiration) -> <>:: * number : Phone number of recipient * expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration. -Exceptions: Failure +Exceptions: Failure, InvalidNumber setContactBlocked(number, block) -> <>:: * number : Phone number affected by method -* block : 0=remove block , 1=blocked +* block : false=remove block, true=blocked Messages from blocked numbers will no longer be forwarded via DBus. @@ -142,11 +150,11 @@ Exceptions: InvalidNumber setGroupBlocked(groupId, block) -> <>:: * groupId : Byte array representing the internal group identifier -* block : 0=remove block , 1=blocked +* block : false=remove block , true=blocked Messages from blocked groups will no longer be forwarded via DBus. -Exceptions: GroupNotFound +Exceptions: GroupNotFound, InvalidGroupId joinGroup(inviteURI) -> <>:: * inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App @@ -158,10 +166,11 @@ quitGroup(groupId) -> <>:: Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember() -Exceptions: GroupNotFound, Failure +Exceptions: GroupNotFound, Failure, InvalidGroupId -isMember(groupId) -> active:: -* groupId : Byte array representing the internal group identifier +isMember(groupId) -> isMember:: +* groupId : Byte array representing the internal group identifier +* isMember : true=you are a group member; false=you are not a group member Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) @@ -174,9 +183,9 @@ sendGroupMessage(message, attachments, groupId) -> timestamp:: * message : Text to send (can be UTF8) * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) * groupId : Byte array representing the internal group identifier -* timestamp : Can be used to identify the corresponding signal reply +* timestamp : Long, can be used to identify the corresponding Signal reply -Exceptions: GroupNotFound, Failure, AttachmentInvalid +Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId sendContacts() -> <>:: @@ -188,12 +197,12 @@ sendSyncRequest() -> <>:: Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device. -Exception: Failure +Exceptions: Failure sendNoteToSelfMessage(message, attachments) -> timestamp:: * message : Text to send (can be UTF8) * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) -* timestamp : Can be used to identify the corresponding signal reply +* timestamp : Long, can be used to identify the corresponding Signal reply Exceptions: Failure, AttachmentInvalid @@ -202,8 +211,8 @@ sendMessage(message, attachments, recipients) -> timestamp:: * message : Text to send (can be UTF8) * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) * recipient : Phone number of a single recipient -* recipients : Array of phone numbers -* timestamp : Can be used to identify the corresponding signal reply +* recipients : String array of phone numbers +* timestamp : Long, can be used to identify the corresponding Signal reply Depending on the type of the recipient field this sends a message to one or multiple recipients. @@ -215,10 +224,9 @@ sendTyping(recipient, stop) -> <>:: Exceptions: Failure, GroupNotFound, UntrustedIdentity - -sendReadReceipt(recipient, targetSentTimestamp) -> <>:: +sendReadReceipt(recipient, targetSentTimestamps) -> <>:: * recipient : Phone number of a single recipient -* targetSentTimestamp : Array of Longs to identify the corresponding signal messages +* targetSentTimestamps : Array of Longs to identify the corresponding Signal messages Exceptions: Failure, UntrustedIdentity @@ -227,10 +235,10 @@ sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimesta * remove : Boolean, whether a previously sent reaction (emoji) should be removed * targetAuthor : String with the phone number of the author of the message to which to react * targetSentTimestamp : Long representing timestamp of the message to which to react -* groupId : Byte array with base64 encoded group identifier +* groupId : Byte array representing the internal group identifier * timestamp : Long, can be used to identify the corresponding signal reply -Exceptions: Failure, InvalidNumber, GroupNotFound +Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipient) -> timestamp:: sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients) -> timestamp:: @@ -240,7 +248,7 @@ sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp * targetSentTimestamp : Long representing timestamp of the message to which to react * recipient : String with the phone number of a single recipient * recipients : Array of strings with phone numbers, should there be more recipients -* timestamp : Long, can be used to identify the corresponding signal reply +* timestamp : Long, can be used to identify the corresponding Signal reply Depending on the type of the recipient(s) field this sends a reaction to one or multiple recipients. @@ -251,7 +259,7 @@ sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId) -> timestamp, recipient) -> timestamp:: sendRemoteDeleteMessage(targetSentTimestamp, recipients) -> timestamp:: @@ -268,66 +276,87 @@ getContactName(number) -> name:: * number : Phone number * name : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used +Exceptions: None + setContactName(number,name<>) -> <>:: * number : Phone number * name : Name to be set in contacts (in local storage with signal-cli) +Exceptions: InvalidNumber, Failure + getGroupIds() -> groupList:: groupList : 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() +Exceptions: None + getGroupName(groupId) -> groupName:: -groupName : The display name of the group -groupId : Byte array representing the internal group identifier +* groupId : Byte array representing the internal group identifier +* groupName : The display name of the group Exceptions: None, if the group name is not found an empty string is returned getGroupMembers(groupId) -> members:: -members : String array with the phone numbers of all active members of a group -groupId : Byte array representing the internal group identifier +* groupId : Byte array representing the internal group identifier +* members : String array with the phone numbers of all active members of a group Exceptions: None, if the group name is not found an empty array is returned listNumbers() -> numbers:: -numbers : String array of all known numbers +* numbers : String array of all known numbers This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) +Exceptions: None + getContactNumber(name) -> numbers:: * numbers : Array of phone number * name : Contact or profile name ("firstname lastname") Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. -isContactBlocked(number) -> state:: -* number : Phone number -* state : 1=blocked, 0=not blocked +Exceptions: None -Exceptions: None, for unknown numbers 0 (false) is returned +isContactBlocked(number) -> blocked:: +* number : Phone number +* blocked : true=blocked, false=not blocked -isGroupBlocked(groupId) -> state:: -* groupId : Byte array representing the internal group identifier -* state : 1=blocked, 0=not blocked +For unknown numbers false is returned but no exception is raised. -Exceptions: None, for unknown groups 0 (false) is returned +Exceptions: InvalidPhoneNumber + +isGroupBlocked(groupId) -> isGroupBlocked:: +* groupId : Byte array representing the internal group identifier +* isGroupBlocked : true=group is blocked; false=group is not blocked + +Dbus will not forward messages from a group when you have blocked it. + +Exceptions: InvalidGroupId, Failure removePin() -> <>:: Removes registration PIN protection. -Exception: Failure +Exceptions: Failure setPin(pin) -> <>:: * pin : PIN you set after registration (resets after 7 days of inactivity) Sets a registration lock PIN, to prevent others from registering your number. -Exception: Failure +Exceptions: Failure version() -> version:: * version : Version string of signal-cli +Exceptions: None + +getSelfNumber() -> number:: +* number : Your phone number + +Exceptions: None + isRegistered() -> result:: isRegistered(number) -> result:: isRegistered(numbers) -> results:: @@ -336,12 +365,12 @@ isRegistered(numbers) -> results:: * result : true=number is registered, false=number is not registered * results : Boolean array of results -Exception: InvalidNumber for an incorrectly formatted phone number. For unknown numbers, false is returned, but no exception is raised. If no number is given, returns whether you are registered (presumably true). +For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered). + +Exceptions: InvalidNumber addDevice(deviceUri) -> <>:: -* deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app - -Exception: InvalidUri +* deviceUri : URI in the form of tsdevice:/?uuid=... Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. listDevices() -> devices:: * devices : Array of structs (objectPath, id, name) @@ -349,25 +378,19 @@ listDevices() -> devices:: ** id : Long representing the deviceId ** name : String representing the device's name -Exception: Failure +Exceptions: InvalidUri -removeDevice(deviceId) -> <>:: -* deviceId : Device ID to remove, obtained from listDevices() command +getDevice(deviceId) -> devicePath:: +* deviceId : Long representing a (potential) deviceId +* devicePath : DBusPath object for the device -Exception: Failure - -updateDeviceName(deviceName) -> <>:: -* deviceName : New name - -Set a new name for this device (main or linked). - -Exception: Failure +Exceptions: DeviceNotFound uploadStickerPack(stickerPackPath) -> url:: * stickerPackPath : Path to the manifest.json file or a zip file in the same directory * url : URL of sticker pack after successful upload -Exception: Failure +Exceptions: Failure submitRateLimitChallenge(challenge, captcha) -> <>:: * challenge : The challenge token taken from the proof required error. @@ -377,7 +400,14 @@ Can be used to lift some rate-limits by solving a captcha. Exception: IOErrorException == Signals -SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: +SyncMessageReceived (timestamp, sender, destination, groupId, message, attachments):: +* timestamp : Integer value that can be used to associate this e.g. with a sendMessage() +* sender : Phone number of the sender +* destination : DBus code for destination +* groupId : Byte array representing the internal group identifier (empty when private message) +* message : Message text +* attachments : String array of filenames in the signal-cli storage (~/.local/share/signal-cli/attachments/) + The sync message is received when the user sends a message from a linked device. ReceiptReceived (timestamp, sender):: @@ -391,7 +421,7 @@ MessageReceived(timestamp, sender, groupId, message, attachments Date: Sat, 9 Oct 2021 11:55:33 -0400 Subject: [PATCH 09/19] Update to new provisioning URL scheme (#762) --- lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java b/lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java index 1f9d10ff..3ba1ef20 100644 --- a/lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java @@ -65,7 +65,7 @@ public class DeviceLinkInfo { public URI createDeviceLinkUri() { final var deviceKeyString = Base64.getEncoder().encodeToString(deviceKey.serialize()).replace("=", ""); try { - return new URI("tsdevice:/?uuid=" + return new URI("sgnl://linkdevice?uuid=" + URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8) + "&pub_key=" + URLEncoder.encode(deviceKeyString, StandardCharsets.UTF_8)); From abd0e718141bd1d4802f8801e4ae801eaa8efdc7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 9 Oct 2021 17:57:32 +0200 Subject: [PATCH 10/19] Update documentation --- man/signal-cli-dbus.5.adoc | 4 ++-- man/signal-cli.1.adoc | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 3d977de6..c5c27fa9 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -58,7 +58,7 @@ link(newDeviceName) -> deviceLinkUri:: * newDeviceName : Name to give new device (defaults to "cli" if no name is given) * deviceLinkUri : URI of newly linked device -Returns a URI of the form "tsdevice:/?uuid=...". This can be piped to a QR encoder to create a display that +Returns a URI of the form "sgnl://linkdevice/?uuid=...". This can be piped to a QR encoder to create a display that can be captured by a Signal smartphone client. 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` @@ -370,7 +370,7 @@ For unknown numbers, false is returned, but no exception is raised. If no number Exceptions: InvalidNumber addDevice(deviceUri) -> <>:: -* deviceUri : URI in the form of tsdevice:/?uuid=... Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. +* deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. listDevices() -> devices:: * devices : Array of structs (objectPath, id, name) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 9829fe00..f2a1a960 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -144,7 +144,7 @@ Remove the registration lock pin. === link Link to an existing device, instead of registering a new number. -This shows a "tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can just use this URI. +This shows a "sgnl://linkdevice/?uuid=..." URI. If you want to connect to another signal-cli instance, you can just use this URI. If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app. *-n* NAME, *--name* NAME:: @@ -158,7 +158,8 @@ Only works, if this is the master device. *--uri* URI:: Specify the uri contained in the QR code shown by the new device. -You will need the full uri enclosed in quotation marks, such as "tsdevice:/?uuid=....." +You will need the full URI such as "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") +Make sure to enclose it in quotation marks for shells. === listDevices From 07742843df5633c800f8bc9106717adc2a0a93a8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Oct 2021 13:30:47 +0200 Subject: [PATCH 11/19] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 269 ++++++++++++++--------- 3 files changed, 160 insertions(+), 111 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 B8dv}T delta 17845 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 X49Luk)baa85H9$5YCsTPT`SVRWMtMW diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f371643e..ffed3a25 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..1b6c7873 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" From bfddb40d73848a767f3a2dfb7a93d7e50172781c Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Oct 2021 13:31:34 +0200 Subject: [PATCH 12/19] Reconnect websockets after errors --- .../org/asamk/signal/manager/SignalWebSocketHealthMonitor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java index 24a673ff..556e227d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java @@ -101,6 +101,7 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor { if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) { logger.warn("Received too many mismatch device errors, forcing new websockets."); signalWebSocket.forceNewWebSockets(); + signalWebSocket.connect(); } } } @@ -146,6 +147,7 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor { + " needed by: " + keepAliveRequiredSinceTime); signalWebSocket.forceNewWebSockets(); + signalWebSocket.connect(); } else { signalWebSocket.sendKeepAlive(); } From 09730b474b11df36d68081ce2937dbf38224c0da Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Oct 2021 13:31:44 +0200 Subject: [PATCH 13/19] Update libsignal-service-java --- lib/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 6e528805..e4e7dc20 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_28") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_29") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") From a95886c4911f8538b53042149312727fb4a3839a Mon Sep 17 00:00:00 2001 From: John Freed Date: Mon, 11 Oct 2021 16:53:24 +0200 Subject: [PATCH 14/19] update DBus documentation (#773) --- man/signal-cli-dbus.5.adoc | 513 +++++++++++++++++++++++-------------- 1 file changed, 325 insertions(+), 188 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index c5c27fa9..f38afc93 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -29,7 +29,7 @@ method(arg1, arg2, ...) -> return Where is according to DBus specification: -* : Array of ... (comma-separated list) +* : Array of ... (comma-separated list) (array:) * (...) : Struct (cannot be sent via `dbus-send`) * : Boolean (false|true) (boolean:) * : Signed 32-bit (int) integer (int32:) @@ -103,50 +103,191 @@ version() -> version:: Exceptions: None -=== Device methods -Requests for these methods are sent to a specific device (main or linked); the list is available -from the listDevices() method (see below under "Other methods"). +=== 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 (+) -removeDevice() -> <>:: +createGroup(groupName, members, avatar) -> groupId:: +* groupName : String representing the display name of the group +* members : String array of new members to be invited to group +* 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; + +getGroup(groupId) -> objectPath:: +* groupId : Byte array representing the internal group identifier +* objectPath : DBusPath for the group + +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 + +Exceptions: None, if the group name is not found an empty array is returned + +joinGroup(inviteURI) -> <>:: +* inviteURI : String starting with https://signal.group/# + +If the link requires admin approval, this adds you to the requesting members list. Otherwise, this adds you to the pending members list. Exceptions: Failure -=== Other methods +listGroups() -> groups:: +* groups : Array of Structs(objectPath, groupId, groupName) +** objectPath : DBusPath +** groupId : Byte array representing the internal group identifier +** groupName : String representing the display name of the group -updateGroup(groupId, newName, members, avatar) -> groupId:: -* groupId : Byte array representing the internal group identifier -* newName : New name of group (empty if unchanged) -* members : String array of new members to be invited to group -* avatar : Filename of avatar picture to be set for group (empty if none) +sendGroupMessage(message, attachments, groupId) -> timestamp:: +* message : Text to send (can be UTF8) +* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) +* groupId : Byte array representing the internal group identifier +* timestamp : Long, can be used to identify the corresponding Signal reply -Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound +Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId -updateProfile(name, about, aboutEmoji , avatar, remove) -> <>:: -updateProfile(givenName, familyName, about, aboutEmoji , avatar, remove) -> <>:: -* name : Name for your own profile (empty if unchanged) -* givenName : Given name for your own profile (empty if unchanged) -* familyName : Family name for your own profile (empty if unchanged) -* about : About message for profile (empty if unchanged) -* aboutEmoji : Emoji for profile (empty if unchanged) -* avatar : Filename of avatar picture for profile (empty if unchanged) -* remove : Set to true if the existing avatar picture should be removed +sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, groupId) -> timestamp:: +* emoji : Unicode grapheme cluster of the emoji +* remove : Boolean, whether a previously sent reaction (emoji) should be removed +* targetAuthor : String with the phone number of the author of the message to which to react +* targetSentTimestamp : Long representing timestamp of the message to which to react +* groupId : Byte array representing the internal group identifier +* timestamp : Long, can be used to identify the corresponding signal reply + +Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId + +sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId) -> timestamp:: +* targetSentTimestamp : Long representing timestamp of the message to delete +* groupId : Byte array with base64 encoded group identifier +* timestamp : Long, can be used to identify the corresponding signal reply + +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 +* Avatar (write-only) : Filename of the avatar +* IsBlocked : true=member will not receive group messages; false=not blocked +* IsMember (read-only) : always true (object path exists only for group members) +* IsAdmin (read-only) : true=member has admin privileges; false=not admin +* MessageExpirationTimer : int32 representing message expiration time for group +* Members (read-only) : String array of group members' phone numbers +* PendingMembers (read-only) : String array of pending members' phone numbers +* RequestingMembers (read-only) : String array of requesting members' phone numbers +* Admins (read-only) : String array of admins' phone numbers +* PermissionAddMember : String representing who has permission to add members +** ONLY_ADMINS, EVERY_MEMBER +* PermissionEditDetails : String representing who may edit group details +** ONLY_ADMINS, EVERY_MEMBER +* PermissionSendMessage : String representing who post messages to group +** ONLY_ADMINS, EVERY_MEMBER (note that ONLY_ADMINS is equivalent to IsAnnouncementGroup) +* GroupInviteLink (read-only) : String of the invitation link (starts with https://signal.group/#) + +To get a property, use (replacing `--session` with `--system` if needed): +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Group string:$PROPERTY_NAME` + +To set a property, use: +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Group string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE` + +To get all properties, use: +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Group` + +addAdmins(recipients) -> <>:: +* recipients : String array of phone numbers + +Grant admit privileges to recipients. Exceptions: Failure +addMembers(recipients) -> <>:: +* recipients : String array of phone numbers -setExpirationTimer(number, expiration) -> <>:: -* number : Phone number of recipient -* expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration. +Add recipients to group if they are pending members; otherwise add recipients to list of requesting members. -Exceptions: Failure, InvalidNumber +Exceptions: Failure -setContactBlocked(number, block) -> <>:: -* number : Phone number affected by method -* block : false=remove block, true=blocked +disableLink() -> <>:: -Messages from blocked numbers will no longer be forwarded via DBus. +Disables the group's invitation link. -Exceptions: InvalidNumber +Exceptions: Failure + +enableLink(requiresApproval) -> <>:: +* requiresApproval : true=add numbers using the link to the requesting members list + +Enables the group's invitation link. + +Exceptions: Failure + +quitGroup() -> <>:: +Exceptions: Failure, LastGroupAdmin + +removeAdmins(recipients) -> <>:: +* recipients : String array of phone numbers + +Remove admin privileges from recipients. + +Exceptions: Failure + +removeMembers(recipients) -> <>:: +* recipients : String array of phone numbers + +Remove recipients from group. + +Exceptions: Failure + +resetLink() -> <>:: + +Resets the group's invitation link to a new random URL starting with https://signal.group/# + +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 + +All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() + +Exceptions: None + +getGroupName(groupId) -> groupName:: +* groupId : Byte array representing the internal group identifier +* groupName : The display name of the group + +Exceptions: None, if the group name is not found an empty string is returned + +isGroupBlocked(groupId) -> isGroupBlocked:: +* groupId : Byte array representing the internal group identifier +* isGroupBlocked : true=group is blocked; false=group is not blocked + +Dbus will not forward messages from a group when you have blocked it. + +Exceptions: InvalidGroupId, Failure + +isMember(groupId) -> isMember:: +* groupId : Byte array representing the internal group identifier +* isMember : true=you are a group member; false=you are not a group member + +Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) + +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() + +Exceptions: GroupNotFound, Failure, InvalidGroupId setGroupBlocked(groupId, block) -> <>:: * groupId : Byte array representing the internal group identifier @@ -156,36 +297,35 @@ Messages from blocked groups will no longer be forwarded via DBus. Exceptions: GroupNotFound, InvalidGroupId -joinGroup(inviteURI) -> <>:: -* inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App +updateGroup(groupId, newName, members, avatar) -> groupId:: +* groupId : Byte array representing the internal group identifier +* newName : New name of group (empty if unchanged) +* members : String array of new members to be invited to group +* avatar : Filename of avatar picture to be set for group (empty if none) -Exceptions: Failure +Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound -quitGroup(groupId) -> <>:: -* groupId : Byte array representing the internal group identifier +=== 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 (+) -Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember() +addDevice(deviceUri) -> <>:: +* deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. -Exceptions: GroupNotFound, Failure, InvalidGroupId +getDevice(deviceId) -> devicePath:: +* deviceId : Long representing a deviceId +* devicePath : DBusPath object for the device -isMember(groupId) -> isMember:: -* groupId : Byte array representing the internal group identifier -* isMember : true=you are a group member; false=you are not a group member +Exceptions: DeviceNotFound -Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) +listDevices() -> devices:: +* devices : Array of structs (objectPath, id, name) +** objectPath : DBusPath representing the device's object path +** id : Long representing the deviceId +** name : String representing the device's name -sendEndSessionMessage(recipients) -> <>:: -* recipients : Array of phone numbers - -Exceptions: Failure, InvalidNumber, UntrustedIdentity - -sendGroupMessage(message, attachments, groupId) -> timestamp:: -* message : Text to send (can be UTF8) -* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) -* groupId : Byte array representing the internal group identifier -* timestamp : Long, can be used to identify the corresponding Signal reply - -Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId +Exceptions: InvalidUri sendContacts() -> <>:: @@ -199,12 +339,89 @@ Sends a synchronization request to the primary device (for group, contacts, ...) Exceptions: Failure -sendNoteToSelfMessage(message, attachments) -> timestamp:: -* message : Text to send (can be UTF8) -* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) -* timestamp : Long, can be used to identify the corresponding Signal reply +=== 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) -Exceptions: Failure, AttachmentInvalid +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 +* Name : String representing the display name of the device + +To get a property, use (replacing `--session` with `--system` if needed): +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Device string:$PROPERTY_NAME` + +To set a property, use: +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Device string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE` + +To get all properties, use: +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Device` + +removeDevice() -> <>:: + +Exceptions: Failure + +=== Other methods + +getContactName(number) -> name:: +* number : Phone number +* name : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used + +Exceptions: None + +getContactNumber(name) -> numbers:: +* numbers : Array of phone number +* name : Contact or profile name ("firstname lastname") + +Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. + +Exceptions: None + +getSelfNumber() -> number:: +* number : Your phone number + +Exceptions: None + +isContactBlocked(number) -> blocked:: +* number : Phone number +* blocked : true=blocked, false=not blocked + +For unknown numbers false is returned but no exception is raised. + +Exceptions: InvalidPhoneNumber + +isRegistered() -> result:: +isRegistered(number) -> result:: +isRegistered(numbers) -> results:: +* number : Phone number +* numbers : String array of phone numbers +* result : true=number is registered, false=number is not registered +* results : Boolean array of results + +For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered). + +Exceptions: InvalidNumber + +listNumbers() -> numbers:: +* numbers : String array of all known numbers + +This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) + +Exceptions: None + +removePin() -> <>:: + +Removes registration PIN protection. + +Exceptions: Failure + +sendEndSessionMessage(recipients) -> <>:: +* recipients : Array of phone numbers + +Exceptions: Failure, InvalidNumber, UntrustedIdentity sendMessage(message, attachments, recipient) -> timestamp:: sendMessage(message, attachments, recipients) -> timestamp:: @@ -218,28 +435,6 @@ Depending on the type of the recipient field this sends a message to one or mult Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity -sendTyping(recipient, stop) -> <>:: -* recipient : Phone number of a single recipient -* targetSentTimestamp : True, if typing state should be stopped - -Exceptions: Failure, GroupNotFound, UntrustedIdentity - -sendReadReceipt(recipient, targetSentTimestamps) -> <>:: -* recipient : Phone number of a single recipient -* targetSentTimestamps : Array of Longs to identify the corresponding Signal messages - -Exceptions: Failure, UntrustedIdentity - -sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, groupId) -> timestamp:: -* emoji : Unicode grapheme cluster of the emoji -* remove : Boolean, whether a previously sent reaction (emoji) should be removed -* targetAuthor : String with the phone number of the author of the message to which to react -* targetSentTimestamp : Long representing timestamp of the message to which to react -* groupId : Byte array representing the internal group identifier -* timestamp : Long, can be used to identify the corresponding signal reply - -Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId - sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipient) -> timestamp:: sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients) -> timestamp:: * emoji : Unicode grapheme cluster of the emoji @@ -254,12 +449,18 @@ Depending on the type of the recipient(s) field this sends a reaction to one or Exceptions: Failure, InvalidNumber -sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId) -> timestamp:: -* targetSentTimestamp : Long representing timestamp of the message to delete -* groupId : Byte array with base64 encoded group identifier -* timestamp : Long, can be used to identify the corresponding signal reply +sendNoteToSelfMessage(message, attachments) -> timestamp:: +* message : Text to send (can be UTF8) +* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) +* timestamp : Long, can be used to identify the corresponding Signal reply -Exceptions: Failure, GroupNotFound, InvalidGroupId +Exceptions: Failure, AttachmentInvalid + +sendReadReceipt(recipient, targetSentTimestamps) -> <>:: +* recipient : Phone number of a single recipient +* targetSentTimestamps : Array of Longs to identify the corresponding Signal messages + +Exceptions: Failure, UntrustedIdentity sendRemoteDeleteMessage(targetSentTimestamp, recipient) -> timestamp:: sendRemoteDeleteMessage(targetSentTimestamp, recipients) -> timestamp:: @@ -272,11 +473,19 @@ Depending on the type of the recipient(s) field this deletes a message with one Exceptions: Failure, InvalidNumber -getContactName(number) -> name:: -* number : Phone number -* name : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used +sendTyping(recipient, stop) -> <>:: +* recipient : Phone number of a single recipient +* targetSentTimestamp : True, if typing state should be stopped -Exceptions: None +Exceptions: Failure, GroupNotFound, UntrustedIdentity + +setContactBlocked(number, block) -> <>:: +* number : Phone number affected by method +* block : false=remove block, true=blocked + +Messages from blocked numbers will no longer be forwarded via DBus. + +Exceptions: InvalidNumber setContactName(number,name<>) -> <>:: * number : Phone number @@ -284,61 +493,11 @@ setContactName(number,name<>) -> <>:: Exceptions: InvalidNumber, Failure -getGroupIds() -> groupList:: -groupList : Array of Byte arrays representing the internal group identifiers +setExpirationTimer(number, expiration) -> <>:: +* number : Phone number of recipient +* expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration. -All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() - -Exceptions: None - -getGroupName(groupId) -> groupName:: -* groupId : Byte array representing the internal group identifier -* groupName : The display name of the group - -Exceptions: None, if the group name is not found an empty string is returned - -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 - -Exceptions: None, if the group name is not found an empty array is returned - -listNumbers() -> numbers:: -* numbers : String array of all known numbers - -This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) - -Exceptions: None - -getContactNumber(name) -> numbers:: -* numbers : Array of phone number -* name : Contact or profile name ("firstname lastname") - -Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. - -Exceptions: None - -isContactBlocked(number) -> blocked:: -* number : Phone number -* blocked : true=blocked, false=not blocked - -For unknown numbers false is returned but no exception is raised. - -Exceptions: InvalidPhoneNumber - -isGroupBlocked(groupId) -> isGroupBlocked:: -* groupId : Byte array representing the internal group identifier -* isGroupBlocked : true=group is blocked; false=group is not blocked - -Dbus will not forward messages from a group when you have blocked it. - -Exceptions: InvalidGroupId, Failure - -removePin() -> <>:: - -Removes registration PIN protection. - -Exceptions: Failure +Exceptions: Failure, InvalidNumber setPin(pin) -> <>:: * pin : PIN you set after registration (resets after 7 days of inactivity) @@ -347,51 +506,6 @@ Sets a registration lock PIN, to prevent others from registering your number. Exceptions: Failure -version() -> version:: -* version : Version string of signal-cli - -Exceptions: None - -getSelfNumber() -> number:: -* number : Your phone number - -Exceptions: None - -isRegistered() -> result:: -isRegistered(number) -> result:: -isRegistered(numbers) -> results:: -* number : Phone number -* numbers : String array of phone numbers -* result : true=number is registered, false=number is not registered -* results : Boolean array of results - -For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered). - -Exceptions: InvalidNumber - -addDevice(deviceUri) -> <>:: -* deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. - -listDevices() -> devices:: -* devices : Array of structs (objectPath, id, name) -** objectPath : DBusPath representing the device's object path -** id : Long representing the deviceId -** name : String representing the device's name - -Exceptions: InvalidUri - -getDevice(deviceId) -> devicePath:: -* deviceId : Long representing a (potential) deviceId -* devicePath : DBusPath object for the device - -Exceptions: DeviceNotFound - -uploadStickerPack(stickerPackPath) -> url:: -* stickerPackPath : Path to the manifest.json file or a zip file in the same directory -* url : URL of sticker pack after successful upload - -Exceptions: Failure - submitRateLimitChallenge(challenge, captcha) -> <>:: * challenge : The challenge token taken from the proof required error. * captcha : The captcha token from the solved captcha on the Signal website.. @@ -399,6 +513,29 @@ Can be used to lift some rate-limits by solving a captcha. Exception: IOErrorException +updateProfile(name, about, aboutEmoji , avatar, remove) -> <>:: +updateProfile(givenName, familyName, about, aboutEmoji , avatar, remove) -> <>:: +* name : Name for your own profile (empty if unchanged) +* givenName : Given name for your own profile (empty if unchanged) +* familyName : Family name for your own profile (empty if unchanged) +* about : About message for profile (empty if unchanged) +* aboutEmoji : Emoji for profile (empty if unchanged) +* avatar : Filename of avatar picture for profile (empty if unchanged) +* remove : Set to true if the existing avatar picture should be removed + +Exceptions: Failure + +uploadStickerPack(stickerPackPath) -> url:: +* stickerPackPath : Path to the manifest.json file or a zip file in the same directory +* url : URL of sticker pack after successful upload + +Exceptions: Failure + +version() -> version:: +* version : Version string of signal-cli + +Exceptions: None + == Signals SyncMessageReceived (timestamp, sender, destination, groupId, message, attachments):: * timestamp : Integer value that can be used to associate this e.g. with a sendMessage() From 15c66684c10bec8650b511decf2f86251f671050 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 12 Oct 2021 18:25:03 +0200 Subject: [PATCH 15/19] Update graalvm buildtools --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 51b2ef75..5d18e9e0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ plugins { application eclipse `check-lib-versions` - id("org.graalvm.buildtools.native") version "0.9.5" + id("org.graalvm.buildtools.native") version "0.9.6" } version = "0.9.0" From e977f38bdd9d955eafe557d1135d36fdf3056306 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 12 Oct 2021 20:48:56 +0200 Subject: [PATCH 16/19] Refactor to remove ProfileKeyProvider and UnidentifiedAccessSenderCertificateProvider --- .../org/asamk/signal/manager/ManagerImpl.java | 25 ++-------- .../signal/manager/helper/ProfileHelper.java | 5 +- .../manager/helper/ProfileKeyProvider.java | 9 ---- .../helper/UnidentifiedAccessHelper.java | 46 +++++++++++++------ ...tifiedAccessSenderCertificateProvider.java | 6 --- 5 files changed, 38 insertions(+), 53 deletions(-) delete mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java delete mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 8ccab36b..f88469ac 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -176,14 +176,13 @@ public class ManagerImpl implements Manager { this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); - final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, - account.getProfileStore()::getProfileKey, - this::getRecipientProfile, - this::getSenderCertificate); + final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account, + dependencies, + account::getProfileKey, + this::getRecipientProfile); this.profileHelper = new ProfileHelper(account, dependencies, avatarStore, - account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, this::resolveSignalServiceAddress); final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, @@ -782,22 +781,6 @@ public class ManagerImpl implements Manager { } } - private byte[] getSenderCertificate() { - byte[] certificate; - try { - if (account.isPhoneNumberShared()) { - certificate = dependencies.getAccountManager().getSenderCertificate(); - } else { - certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); - } - } catch (IOException e) { - logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); - return null; - } - // TODO cache for a day - return certificate; - } - private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { final var address = resolveSignalServiceAddress(recipientId); if (!address.getNumber().isPresent()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index e24d41fa..7a492d9f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -45,7 +45,6 @@ public final class ProfileHelper { private final SignalAccount account; private final SignalDependencies dependencies; private final AvatarStore avatarStore; - private final ProfileKeyProvider profileKeyProvider; private final UnidentifiedAccessProvider unidentifiedAccessProvider; private final SignalServiceAddressResolver addressResolver; @@ -53,14 +52,12 @@ public final class ProfileHelper { final SignalAccount account, final SignalDependencies dependencies, final AvatarStore avatarStore, - final ProfileKeyProvider profileKeyProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider, final SignalServiceAddressResolver addressResolver ) { this.account = account; this.dependencies = dependencies; this.avatarStore = avatarStore; - this.profileKeyProvider = profileKeyProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider; this.addressResolver = addressResolver; } @@ -296,7 +293,7 @@ public final class ProfileHelper { RecipientId recipientId, SignalServiceProfile.RequestType requestType ) { var unidentifiedAccess = getUnidentifiedAccess(recipientId); - var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId)); + var profileKey = Optional.fromNullable(account.getProfileStore().getProfileKey(recipientId)); final var address = addressResolver.resolveSignalServiceAddress(recipientId); return retrieveProfile(address, profileKey, unidentifiedAccess, requestType); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java deleted file mode 100644 index b98d674e..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.asamk.signal.manager.helper; - -import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.signal.zkgroup.profiles.ProfileKey; - -public interface ProfileKeyProvider { - - ProfileKey getProfileKey(RecipientId address); -} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index 87e23c1b..661a7f96 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -1,11 +1,16 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.signal.libsignal.metadata.certificate.InvalidCertificateException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @@ -13,24 +18,39 @@ import static org.whispersystems.signalservice.internal.util.Util.getSecretBytes public class UnidentifiedAccessHelper { + private final static Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; private final SelfProfileKeyProvider selfProfileKeyProvider; - - private final ProfileKeyProvider profileKeyProvider; - private final ProfileProvider profileProvider; - private final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider; - public UnidentifiedAccessHelper( + final SignalAccount account, + final SignalDependencies dependencies, final SelfProfileKeyProvider selfProfileKeyProvider, - final ProfileKeyProvider profileKeyProvider, - final ProfileProvider profileProvider, - final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider + final ProfileProvider profileProvider ) { + this.account = account; + this.dependencies = dependencies; this.selfProfileKeyProvider = selfProfileKeyProvider; - this.profileKeyProvider = profileKeyProvider; this.profileProvider = profileProvider; - this.senderCertificateProvider = senderCertificateProvider; + } + + private byte[] getSenderCertificate() { + byte[] certificate; + try { + if (account.isPhoneNumberShared()) { + certificate = dependencies.getAccountManager().getSenderCertificate(); + } else { + certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); + } + } catch (IOException e) { + logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); + return null; + } + // TODO cache for a day + return certificate; } private byte[] getSelfUnidentifiedAccessKey() { @@ -45,7 +65,7 @@ public class UnidentifiedAccessHelper { switch (targetProfile.getUnidentifiedAccessMode()) { case ENABLED: - var theirProfileKey = profileKeyProvider.getProfileKey(recipient); + var theirProfileKey = account.getProfileStore().getProfileKey(recipient); if (theirProfileKey == null) { return null; } @@ -60,7 +80,7 @@ public class UnidentifiedAccessHelper { public Optional getAccessForSync() { var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); - var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate(); + var selfUnidentifiedAccessCertificate = getSenderCertificate(); if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) { return Optional.absent(); @@ -82,7 +102,7 @@ public class UnidentifiedAccessHelper { public Optional getAccessFor(RecipientId recipient) { var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); - var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate(); + var selfUnidentifiedAccessCertificate = getSenderCertificate(); if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java deleted file mode 100644 index b0597346..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.asamk.signal.manager.helper; - -public interface UnidentifiedAccessSenderCertificateProvider { - - byte[] getSenderCertificate(); -} From 997b3c6a2a1f2e9a50fe579ac726677ea0d57d0c Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 12 Oct 2021 20:49:41 +0200 Subject: [PATCH 17/19] Restrict blocking of group to master device --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 2 +- lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java | 5 ++++- src/main/java/org/asamk/signal/commands/BlockCommand.java | 2 ++ src/main/java/org/asamk/signal/commands/UnblockCommand.java | 2 ++ src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 4 ++++ 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index f529b408..3cfeb4f7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -184,7 +184,7 @@ public interface Manager extends Closeable { void setGroupBlocked( GroupId groupId, boolean blocked - ) throws GroupNotFoundException, IOException; + ) throws GroupNotFoundException, IOException, NotMasterDeviceException; void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index f88469ac..a14f2d1f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -714,7 +714,10 @@ public class ManagerImpl implements Manager { @Override public void setGroupBlocked( final GroupId groupId, final boolean blocked - ) throws GroupNotFoundException, IOException { + ) throws GroupNotFoundException, IOException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } groupHelper.setGroupBlocked(groupId, blocked); // TODO cycle our profile key syncHelper.sendBlockedList(); diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 516224f5..1ec1036c 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -52,6 +52,8 @@ public class BlockCommand implements JsonRpcLocalCommand { for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) { try { m.setGroupBlocked(groupId, true); + } catch (NotMasterDeviceException e) { + throw new UserErrorException("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage()); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 7cf209fa..53eab3b3 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -51,6 +51,8 @@ public class UnblockCommand implements JsonRpcLocalCommand { for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) { try { m.setGroupBlocked(groupId, false); + } catch (NotMasterDeviceException e) { + throw new UserErrorException("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { logger.warn("Unknown group id: {}", groupId); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 56ecdc55..2cf3c813 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -405,6 +405,8 @@ public class DbusSignalImpl implements Signal { public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { m.setGroupBlocked(getGroupId(groupId), blocked); + } catch (NotMasterDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException e) { @@ -1060,6 +1062,8 @@ public class DbusSignalImpl implements Signal { private void setIsBlocked(final boolean isBlocked) { try { m.setGroupBlocked(groupId, isBlocked); + } catch (NotMasterDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException e) { From f094cd6806aae68fba9ebd0cf29eaf4eabf042d2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 12 Oct 2021 22:14:39 +0200 Subject: [PATCH 18/19] Extract IdentityHelper --- .../org/asamk/signal/manager/Manager.java | 3 - .../org/asamk/signal/manager/ManagerImpl.java | 99 +++---------- .../signal/manager/helper/IdentityHelper.java | 135 ++++++++++++++++++ .../asamk/signal/ReceiveMessageHandler.java | 4 - .../asamk/signal/dbus/DbusManagerImpl.java | 8 -- 5 files changed, 151 insertions(+), 98 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 3cfeb4f7..733e3dcc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -22,7 +22,6 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; -import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -228,8 +227,6 @@ public interface Manager extends Closeable { boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient); - String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index a14f2d1f..d2ffaaab 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -38,6 +38,7 @@ import org.asamk.signal.manager.helper.AttachmentHelper; import org.asamk.signal.manager.helper.ContactHelper; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; +import org.asamk.signal.manager.helper.IdentityHelper; import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.PreKeyHelper; @@ -59,15 +60,10 @@ import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; -import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.fingerprint.Fingerprint; -import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; -import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; @@ -98,9 +94,7 @@ import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SignatureException; -import java.util.Arrays; import java.util.Collection; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -112,7 +106,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Function; import java.util.stream.Collectors; import static org.asamk.signal.manager.config.ServiceConfig.capabilities; @@ -138,6 +131,7 @@ public class ManagerImpl implements Manager { private final ContactHelper contactHelper; private final IncomingMessageHandler incomingMessageHandler; private final PreKeyHelper preKeyHelper; + private final IdentityHelper identityHelper; private final Context context; private boolean hasCaughtUpWithOldMessages = false; @@ -238,6 +232,11 @@ public class ManagerImpl implements Manager { syncHelper, this::getRecipientProfile, jobExecutor); + this.identityHelper = new IdentityHelper(account, + dependencies, + this::resolveSignalServiceAddress, + syncHelper, + profileHelper); } @Override @@ -1042,7 +1041,7 @@ public class ManagerImpl implements Manager { return toGroup(groupHelper.getGroup(groupId)); } - public GroupInfo getGroupInfo(GroupId groupId) { + private GroupInfo getGroupInfo(GroupId groupId) { return groupHelper.getGroup(groupId); } @@ -1063,8 +1062,9 @@ public class ManagerImpl implements Manager { final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); return new Identity(address, identityInfo.getIdentityKey(), - computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), - computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), + identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), + identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), + identityInfo.getIdentityKey()).getSerialized(), identityInfo.getTrustLevel(), identityInfo.getDateAdded()); } @@ -1094,9 +1094,7 @@ public class ManagerImpl implements Manager { } catch (UnregisteredUserException e) { return false; } - return trustIdentity(recipientId, - identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), - TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerified(recipientId, fingerprint); } /** @@ -1113,10 +1111,7 @@ public class ManagerImpl implements Manager { } catch (UnregisteredUserException e) { return false; } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, - identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), - TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); } /** @@ -1133,15 +1128,7 @@ public class ManagerImpl implements Manager { } catch (UnregisteredUserException e) { return false; } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, identityKey -> { - final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); - try { - return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber); - } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { - return false; - } - }, TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); } /** @@ -1157,66 +1144,13 @@ public class ManagerImpl implements Manager { } catch (UnregisteredUserException e) { return false; } - return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); - } - - private boolean trustIdentity( - RecipientId recipientId, Function verifier, TrustLevel trustLevel - ) { - var identity = account.getIdentityKeyStore().getIdentity(recipientId); - if (identity == null) { - return false; - } - - if (!verifier.apply(identity.getIdentityKey())) { - return false; - } - - account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); - try { - var address = resolveSignalServiceAddress(recipientId); - syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); - } catch (IOException e) { - logger.warn("Failed to send verification sync message: {}", e.getMessage()); - } - - return true; + return identityHelper.trustIdentityAllKeys(recipientId); } private void handleIdentityFailure( final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure ) { - final var identityKey = identityFailure.getIdentityKey(); - if (identityKey != null) { - final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } else { - // Retrieve profile to get the current identity key from the server - profileHelper.refreshRecipientProfile(recipientId); - } - } - - @Override - public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); - } - - private byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); - } - - private Fingerprint computeSafetyNumberFingerprint( - final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey - ) { - return Utils.computeSafetyNumber(capabilities.isUuid(), - account.getSelfAddress(), - account.getIdentityKeyPair().getPublicKey(), - theirAddress, - theirIdentityKey); + this.identityHelper.handleIdentityFailure(recipientId, identityFailure); } @Override @@ -1291,5 +1225,4 @@ public class ManagerImpl implements Manager { } account = null; } - } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java new file mode 100644 index 00000000..531870d9 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java @@ -0,0 +1,135 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; +import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; +import org.whispersystems.libsignal.fingerprint.ScannableFingerprint; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.function.Function; + +import static org.asamk.signal.manager.config.ServiceConfig.capabilities; + +public class IdentityHelper { + + private final static Logger logger = LoggerFactory.getLogger(IdentityHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final SignalServiceAddressResolver addressResolver; + private final SyncHelper syncHelper; + private final ProfileHelper profileHelper; + + public IdentityHelper( + final SignalAccount account, + final SignalDependencies dependencies, + final SignalServiceAddressResolver addressResolver, + final SyncHelper syncHelper, + final ProfileHelper profileHelper + ) { + this.account = account; + this.dependencies = dependencies; + this.addressResolver = addressResolver; + this.syncHelper = syncHelper; + this.profileHelper = profileHelper; + } + + public boolean trustIdentityVerified(RecipientId recipientId, byte[] fingerprint) { + return trustIdentity(recipientId, + identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), + TrustLevel.TRUSTED_VERIFIED); + } + + public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, String safetyNumber) { + return trustIdentity(recipientId, + identityKey -> safetyNumber.equals(computeSafetyNumber(recipientId, identityKey)), + TrustLevel.TRUSTED_VERIFIED); + } + + public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, byte[] safetyNumber) { + return trustIdentity(recipientId, identityKey -> { + final var fingerprint = computeSafetyNumberForScanning(recipientId, identityKey); + try { + return fingerprint != null && fingerprint.compareTo(safetyNumber); + } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { + return false; + } + }, TrustLevel.TRUSTED_VERIFIED); + } + + public boolean trustIdentityAllKeys(RecipientId recipientId) { + return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); + } + + public String computeSafetyNumber(RecipientId recipientId, IdentityKey theirIdentityKey) { + var address = addressResolver.resolveSignalServiceAddress(recipientId); + final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); + } + + public ScannableFingerprint computeSafetyNumberForScanning(RecipientId recipientId, IdentityKey theirIdentityKey) { + var address = addressResolver.resolveSignalServiceAddress(recipientId); + final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getScannableFingerprint(); + } + + private Fingerprint computeSafetyNumberFingerprint( + final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey + ) { + return Utils.computeSafetyNumber(capabilities.isUuid(), + account.getSelfAddress(), + account.getIdentityKeyPair().getPublicKey(), + theirAddress, + theirIdentityKey); + } + + private boolean trustIdentity( + RecipientId recipientId, Function verifier, TrustLevel trustLevel + ) { + var identity = account.getIdentityKeyStore().getIdentity(recipientId); + if (identity == null) { + return false; + } + + if (!verifier.apply(identity.getIdentityKey())) { + return false; + } + + account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); + try { + var address = addressResolver.resolveSignalServiceAddress(recipientId); + syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); + } catch (IOException e) { + logger.warn("Failed to send verification sync message: {}", e.getMessage()); + } + + return true; + } + + public void handleIdentityFailure( + final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure + ) { + final var identityKey = identityFailure.getIdentityKey(); + if (identityKey != null) { + final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } else { + // Retrieve profile to get the current identity key from the server + profileHelper.refreshRecipientProfile(recipientId); + } + } +} diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 35790678..b1580b34 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -6,7 +6,6 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.DateUtils; -import org.asamk.signal.util.Util; import org.slf4j.helpers.MessageFormatter; import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -377,9 +376,6 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { writer.println("Received sync message with verified identities:"); final var verifiedMessage = syncMessage.getVerified().get(); writer.println("- {}: {}", formatContact(verifiedMessage.getDestination()), verifiedMessage.getVerified()); - var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), - verifiedMessage.getIdentityKey())); - writer.indentedWriter().println(safetyNumber); } if (syncMessage.getConfiguration().isPresent()) { writer.println("Received sync message with configuration:"); diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 6e655bdc..59422e69 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -30,7 +30,6 @@ import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBusInterface; -import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -538,13 +537,6 @@ public class DbusManagerImpl implements Manager { throw new UnsupportedOperationException(); } - @Override - public String computeSafetyNumber( - final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey - ) { - throw new UnsupportedOperationException(); - } - @Override public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) { return address; From 18ad9fbb4ec06b20be9043ae462997b266b8de1e Mon Sep 17 00:00:00 2001 From: John Freed Date: Wed, 13 Oct 2021 08:00:07 +0200 Subject: [PATCH 19/19] fix typos in DBus doc (#774) and expand on functioning of joinGroup method --- man/signal-cli-dbus.5.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index f38afc93..55058580 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -129,7 +129,7 @@ Exceptions: None, if the group name is not found an empty array is returned joinGroup(inviteURI) -> <>:: * inviteURI : String starting with https://signal.group/# -If the link requires admin approval, this adds you to the requesting members list. Otherwise, this adds you to the pending members list. +Behavior of this method depends on the `requirePermission` parameter of the `enableLink` method. If permission is required, `joinGroup` adds you to the requesting members list. Permission may be granted based on the group's `PermissionAddMember` property (`ONLY_ADMINS` or `EVERY_MEMBER`). If permission is not required, `joinGroup` admits you immediately to the group. Exceptions: Failure @@ -203,7 +203,7 @@ To get all properties, use: addAdmins(recipients) -> <>:: * recipients : String array of phone numbers -Grant admit privileges to recipients. +Grant admin privileges to recipients. Exceptions: Failure