From d622967192ce03d3650f2cbc582c6555ab0ca23c Mon Sep 17 00:00:00 2001 From: John Freed Date: Tue, 21 Sep 2021 22:26:26 +0200 Subject: [PATCH 01/26] Implement Dbus setPin and removePin (#733) and update documentation --- .gitignore | 1 + man/signal-cli-dbus.5.adoc | 13 ++++++++++ src/main/java/org/asamk/Signal.java | 7 ++++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 24 +++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/.gitignore b/.gitignore index 8fa9c8bd..e41d1e40 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ local.properties .settings/ out/ .DS_Store +/bin/ diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 4ff5e994..d562d064 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -229,6 +229,19 @@ isGroupBlocked(groupId) -> state:: Exceptions: None, for unknown groups 0 (false) is returned +removePin() -> <>:: + +Removes registration PIN protection. + +Exception: 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 + version() -> version:: * version : Version string of signal-cli diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 868de02b..a30f8f3b 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,10 +1,13 @@ package org.asamk; +import org.asamk.Signal.Error; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.messages.DBusSignal; +import org.whispersystems.libsignal.util.guava.Optional; +import java.io.IOException; import java.util.List; /** @@ -87,6 +90,10 @@ public interface Signal extends DBusInterface { String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar ) throws Error.Failure; + void removePin(); + + void setPin(String registrationLockPin); + String version(); List listNumbers(); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 5e8fd432..44250d5b 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -18,6 +18,7 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; + import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -25,6 +26,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.File; import java.io.IOException; @@ -413,6 +415,28 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void removePin() { + try { + m.setRegistrationLockPin(Optional.absent()); + } catch (UnauthenticatedResponseException e) { + throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage()); + } catch (IOException e) { + throw new Error.Failure("Remove pin error: " + e.getMessage()); + } + } + + @Override + public void setPin(String registrationLockPin) { + try { + m.setRegistrationLockPin(Optional.of(registrationLockPin)); + } catch (UnauthenticatedResponseException e) { + throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage()); + } catch (IOException e) { + throw new Error.Failure("Set pin error: " + e.getMessage()); + } + } + // Provide option to query a version string in order to react on potential // future interface changes @Override From 982e887c9ffaa58cc018fdcdb8ffc5f0251ba04f Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 21 Sep 2021 22:30:27 +0200 Subject: [PATCH 02/26] Reformat code --- src/main/java/org/asamk/Signal.java | 3 --- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index a30f8f3b..821e04d9 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,13 +1,10 @@ package org.asamk; -import org.asamk.Signal.Error; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.messages.DBusSignal; -import org.whispersystems.libsignal.util.guava.Optional; -import java.io.IOException; import java.util.List; /** diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 44250d5b..89703387 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -18,7 +18,6 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; - import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -427,7 +426,7 @@ public class DbusSignalImpl implements Signal { } @Override - public void setPin(String registrationLockPin) { + public void setPin(String registrationLockPin) { try { m.setRegistrationLockPin(Optional.of(registrationLockPin)); } catch (UnauthenticatedResponseException e) { From 1ca0e75ef185a5f690162ff82e22732052e9ef57 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 08:59:38 +0200 Subject: [PATCH 03/26] implement Dbus stickerpack method (#740) implement uploadStickerPack update documentation --- man/signal-cli-dbus.5.adoc | 6 ++++++ src/main/java/org/asamk/Signal.java | 2 ++ .../java/org/asamk/signal/dbus/DbusSignalImpl.java | 13 +++++++++++++ 3 files changed, 21 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index d562d064..b7dfcfe1 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -248,6 +248,12 @@ version() -> version:: isRegistred -> result:: * result : Currently always returns 1=true +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 + == Signals SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 821e04d9..1eb96510 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -107,6 +107,8 @@ public interface Signal extends DBusInterface { byte[] joinGroup(final String groupLink) throws Error.Failure; + String uploadStickerPack(String stickerPackPath) throws Error.Failure; + 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 89703387..0fde767d 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -5,6 +5,7 @@ import org.asamk.signal.BaseConfig; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; +import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -536,6 +537,18 @@ public class DbusSignalImpl implements Signal { } } + @Override + public String uploadStickerPack(String stickerPackPath) { + File path = new File(stickerPackPath); + try { + return m.uploadStickerPack(path).toString(); + } catch (IOException e) { + throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage()); + } catch (StickerPackInvalidException e) { + throw new Error.Failure("Invalid sticker pack: " + e.getMessage()); + } + } + private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { var error = ErrorUtils.getErrorMessageFromSendMessageResult(result); From 8bee08fd96571f0f08ffa713b7bd20a2b251d277 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 09:00:26 +0200 Subject: [PATCH 04/26] implement Dbus sync methods (#737) implement two Dbus methods: - sendContacts - sendSyncRequest update documentation --- man/signal-cli-dbus.5.adoc | 12 ++++++++++++ src/main/java/org/asamk/Signal.java | 4 ++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index b7dfcfe1..8cc234bc 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -107,6 +107,18 @@ sendGroupMessage(message, attachments, groupId) -> timestamp:: Exceptions: GroupNotFound, Failure, AttachmentInvalid +sendContacts() -> <>:: + +Sends a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the primary device. + +Exceptions: Failure + +sendSyncRequest() -> <>:: + +Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device. + +Exception: 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) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 1eb96510..d981e024 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -49,6 +49,10 @@ public interface Signal extends DBusInterface { String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients ) throws Error.InvalidNumber, Error.Failure; + void sendContacts() throws Error.Failure; + + void sendSyncRequest() throws Error.Failure; + long sendNoteToSelfMessage( String message, List attachments ) throws Error.AttachmentInvalid, Error.Failure; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 0fde767d..4a478f13 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -202,6 +202,24 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void sendContacts() { + try { + m.sendContacts(); + } catch (IOException e) { + throw new Error.Failure("SendContacts error: " + e.getMessage()); + } + } + + @Override + public void sendSyncRequest() { + try { + m.requestAllSyncData(); + } catch (IOException e) { + throw new Error.Failure("Request sync data error: " + e.getMessage()); + } + } + @Override public long sendNoteToSelfMessage( final String message, final List attachments From d47574351e0e27cf308ddacac2b3abf597d34fcb Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 09:04:40 +0200 Subject: [PATCH 05/26] implement Dbus setExpirationTimer (#735) implement method update documentation --- man/signal-cli-dbus.5.adoc | 7 +++++++ src/main/java/org/asamk/Signal.java | 2 ++ src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 9 +++++++++ 3 files changed, 18 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 8cc234bc..12b87d2b 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -61,6 +61,13 @@ updateProfile(newName, about , aboutEmoji , avatar, remove) -> <> Exceptions: Failure + +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 + setContactBlocked(number, block) -> <>:: * number : Phone number affected by method * block : 0=remove block , 1=blocked diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index d981e024..c5839d14 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -71,6 +71,8 @@ public interface Signal extends DBusInterface { void setContactName(String number, String name) throws Error.InvalidNumber; + void setExpirationTimer(final String number, final int expiration) throws Error.Failure; + void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 4a478f13..dfd55f62 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -305,6 +305,15 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void setExpirationTimer(final String number, final int expiration) { + try { + m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getUsername()), expiration); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + @Override public void setContactBlocked(final String number, final boolean blocked) { try { From e78463ea0a81ef326b6adf85a50887de5192fcf7 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 09:26:12 +0200 Subject: [PATCH 06/26] implement Dbus updateAccount and listDevices (#730) * implement Dbus updateAccount and listDevices implement updateAccount(deviceName) to change device name implement listDevices update documentation * implement Dbus addDevice and removeDevice update documentation as well * Dbus add/remove/list/update devices modifications responding to requests by AsamK * Dbus incorporating InvalidUri error Co-authored-by: AsamK --- man/signal-cli-dbus.5.adoc | 80 +++++++++++++++++++ src/main/java/org/asamk/Signal.java | 15 ++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 49 ++++++++++++ 3 files changed, 144 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 12b87d2b..5d65c48f 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -44,6 +44,64 @@ 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 (_). +Only `version()` is activated in single-user mode; the rest are disabled. + +link() -> deviceLinkUri:: +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 +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 + +listAccounts() -> accountList:: +* accountList : Array of all attached accounts in DBus object path form + +Exceptions: None + +register(number, voiceVerification) -> <>:: +* number : Phone number +* voiceVerification : true = use voice verification; false = use SMS verification + +Exceptions: Failure, InvalidNumber, RequiresCaptcha + +registerWithCaptcha(number, voiceVerification, captcha) -> <>:: +* number : Phone number +* voiceVerification : true = use voice verification; false = use SMS verification +* captcha : Captcha string + +Exceptions: Failure, InvalidNumber, RequiresCaptcha + +verify(number, verificationCode) -> <>:: +* number : Phone number +* verificationCode : Code received from Signal after successful registration request + +Command fails if PIN was set after previous registration; use verifyWithPin instead. + +Exception: 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 + +version() -> version:: +* version : Version string of signal-cli + +Exceptions: None + +=== Other methods + updateGroup(groupId, newName, members, avatar) -> groupId:: * groupId : Byte array representing the internal group identifier * newName : New name of group (empty if unchanged) @@ -267,6 +325,28 @@ version() -> version:: isRegistred -> result:: * result : Currently always returns 1=true +addDevice(deviceUri) -> <>:: +* deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app + +Exception: InvalidUri + +listDevices() -> devices:: +* devices : String array of linked devices + +Exception: Failure + +removeDevice(deviceId) -> <>:: +* deviceId : Device ID to remove, obtained from listDevices() command + +Exception: Failure + +updateDeviceName(deviceName) -> <>:: +* deviceName : New name + +Set a new name for this device (main or linked). + +Exception: 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 diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index c5839d14..55585c0d 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -89,6 +89,14 @@ public interface Signal extends DBusInterface { boolean isRegistered(); + void addDevice(String uri) throws Error.InvalidUri; + + void removeDevice(int deviceId) throws Error.Failure; + + List listDevices() throws Error.Failure; + + void updateDeviceName(String deviceName) throws Error.Failure; + void updateProfile( String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar ) throws Error.Failure; @@ -241,6 +249,13 @@ public interface Signal extends DBusInterface { } } + class InvalidUri extends DBusExecutionException { + + public InvalidUri(final String message) { + super(message); + } + } + class Failure extends DBusExecutionException { public Failure(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 dfd55f62..768f6e89 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -7,6 +7,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; +import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; @@ -20,6 +21,7 @@ import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; @@ -30,6 +32,8 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -62,6 +66,51 @@ public class DbusSignalImpl implements Signal { return objectPath; } + @Override + public void addDevice(String uri) { + try { + m.addDeviceLink(new URI(uri)); + } catch (IOException | InvalidKeyException e) { + throw new Error.Failure(e.getClass().getSimpleName() + " Add device link failed. " + e.getMessage()); + } catch (URISyntaxException e) { + throw new Error.InvalidUri(e.getClass().getSimpleName() + " Device link uri has invalid format: " + e.getMessage()); + } + } + + @Override + public void removeDevice(int deviceId) { + try { + m.removeLinkedDevices(deviceId); + } catch (IOException e) { + throw new Error.Failure(e.getClass().getSimpleName() + ": Error while removing device: " + e.getMessage()); + } + } + + @Override + public List listDevices() { + List devices; + List results = new ArrayList(); + + try { + devices = m.getLinkedDevices(); + } catch (IOException | Error.Failure e) { + throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); + } + + return devices.stream() + .map(d -> d.getName() == null ? "" : d.getName()) + .collect(Collectors.toList()); + } + + @Override + public void updateDeviceName(String deviceName) { + try { + m.updateAccountAttributes(deviceName); + } catch (IOException | Signal.Error.Failure e) { + throw new Error.Failure("UpdateAccount error: " + e.getMessage()); + } + } + @Override public long sendMessage(final String message, final List attachments, final String recipient) { var recipients = new ArrayList(1); From df8dd54791090b0d9fae82a94af5554f79a7d71d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 26 Sep 2021 09:27:55 +0200 Subject: [PATCH 07/26] Reformat code --- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 768f6e89..7e78d85b 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -73,7 +73,9 @@ public class DbusSignalImpl implements Signal { } catch (IOException | InvalidKeyException e) { throw new Error.Failure(e.getClass().getSimpleName() + " Add device link failed. " + e.getMessage()); } catch (URISyntaxException e) { - throw new Error.InvalidUri(e.getClass().getSimpleName() + " Device link uri has invalid format: " + e.getMessage()); + throw new Error.InvalidUri(e.getClass().getSimpleName() + + " Device link uri has invalid format: " + + e.getMessage()); } } @@ -97,9 +99,7 @@ public class DbusSignalImpl implements Signal { throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); } - return devices.stream() - .map(d -> d.getName() == null ? "" : d.getName()) - .collect(Collectors.toList()); + return devices.stream().map(d -> d.getName() == null ? "" : d.getName()).collect(Collectors.toList()); } @Override From 1c4a32fef4a3273099f0bfdd1b0dea72d32324ae Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 20:09:57 +0200 Subject: [PATCH 08/26] implement Dbus isRegistered() methods (#729) * implement Dbus isRegistered() methods isRegistered(number) returns a boolean isRegistered(numbers) returns an array of Booleans * Dbus isRegistered() methods restore isRegistered() and respond to other requests by AsamK --- man/signal-cli-dbus.5.adoc | 11 +++++-- src/main/java/org/asamk/Signal.java | 6 +++- .../org/asamk/signal/dbus/DbusSignalImpl.java | 29 ++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 5d65c48f..6b5d1a86 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -322,8 +322,15 @@ Exception: Failure version() -> version:: * version : Version string of signal-cli -isRegistred -> result:: -* result : Currently always returns 1=true +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 + +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). addDevice(deviceUri) -> <>:: * deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 55585c0d..3bfeb5bd 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -87,7 +87,11 @@ public interface Signal extends DBusInterface { byte[] groupId, String name, List members, String avatar ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId; - boolean isRegistered(); + boolean isRegistered() throws Error.Failure, Error.InvalidNumber; + + boolean isRegistered(String number) throws Error.Failure, Error.InvalidNumber; + + List isRegistered(List numbers) throws Error.Failure, Error.InvalidNumber; void addDevice(String uri) throws Error.InvalidUri; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 7e78d85b..82cd8f8d 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -467,7 +468,33 @@ public class DbusSignalImpl implements Signal { @Override public boolean isRegistered() { - return true; + var result = isRegistered(List.of(m.getUsername())); + return result.get(0); + } + + @Override + public boolean isRegistered(String number) { + var result = isRegistered(List.of(number)); + return result.get(0); + } + + @Override + public List isRegistered(List numbers) { + var results = new ArrayList (); + Map> registered; + if (numbers.isEmpty()) { + return results; + } + try { + registered = m.areUsersRegistered(new HashSet(numbers)); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + for (String number : numbers) { + UUID uuid = registered.get(number).second(); + results.add(uuid != null); + } + return results; } @Override From 375c9d60cf27d10882bd1a4fd5d5f7ca90eca8ed Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 26 Sep 2021 20:16:27 +0200 Subject: [PATCH 09/26] Refactor isRegistered --- .../org/asamk/signal/dbus/DbusSignalImpl.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 82cd8f8d..63764a2f 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -468,8 +468,7 @@ public class DbusSignalImpl implements Signal { @Override public boolean isRegistered() { - var result = isRegistered(List.of(m.getUsername())); - return result.get(0); + return true; } @Override @@ -480,21 +479,22 @@ public class DbusSignalImpl implements Signal { @Override public List isRegistered(List numbers) { - var results = new ArrayList (); - Map> registered; + var results = new ArrayList(); if (numbers.isEmpty()) { return results; } + + Map> registered; try { - registered = m.areUsersRegistered(new HashSet(numbers)); + registered = m.areUsersRegistered(new HashSet<>(numbers)); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } - for (String number : numbers) { - UUID uuid = registered.get(number).second(); - results.add(uuid != null); - } - return results; + + return numbers.stream().map(number -> { + var uuid = registered.get(number).second(); + return uuid != null; + }).collect(Collectors.toList()); } @Override From ba817e2ae4147b201fbb3e5eb8c86e359873ec02 Mon Sep 17 00:00:00 2001 From: John Freed Date: Tue, 28 Sep 2021 18:41:10 +0200 Subject: [PATCH 10/26] Implement Dbus updateProfile with givenName (#734) two versions of updateProfile implemented: - one with givenName and familyName - one with just name update documentation --- man/signal-cli-dbus.5.adoc | 9 +++++--- src/main/java/org/asamk/Signal.java | 4 ++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 6b5d1a86..e7cd083f 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -110,12 +110,15 @@ updateGroup(groupId, newName, members, avatar) -> groupId:: Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound -updateProfile(newName, about , aboutEmoji , avatar, remove) -> <>:: -* newName : New name for your own profile (empty if unchanged) +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 1 if the existing avatar picture should be removed +* remove : Set to true if the existing avatar picture should be removed Exceptions: Failure diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 3bfeb5bd..59aa03ce 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -101,6 +101,10 @@ public interface Signal extends DBusInterface { void updateDeviceName(String deviceName) throws Error.Failure; + void updateProfile( + String givenName, String familyName, String about, String aboutEmoji, String avatarPath, boolean removeAvatar + ) throws Error.Failure; + void updateProfile( String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar ) throws Error.Failure; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 63764a2f..c73918ef 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -497,6 +497,28 @@ public class DbusSignalImpl implements Signal { }).collect(Collectors.toList()); } + @Override + public void updateProfile( + final String givenName, + final String familyName, + final String about, + final String aboutEmoji, + String avatarPath, + final boolean removeAvatar + ) { + try { + if (avatarPath.isEmpty()) { + avatarPath = null; + } + Optional avatarFile = removeAvatar + ? Optional.absent() + : avatarPath == null ? null : Optional.of(new File(avatarPath)); + m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + @Override public void updateProfile( final String name, From 4acab9043c1f479adf735e193f9404a014b52ed7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 18:42:05 +0200 Subject: [PATCH 11/26] Reformat code --- src/main/java/org/asamk/Signal.java | 7 ++++++- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 59aa03ce..b19fba8d 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -102,7 +102,12 @@ public interface Signal extends DBusInterface { void updateDeviceName(String deviceName) throws Error.Failure; void updateProfile( - String givenName, String familyName, String about, String aboutEmoji, String avatarPath, boolean removeAvatar + String givenName, + String familyName, + String about, + String aboutEmoji, + String avatarPath, + boolean removeAvatar ) throws Error.Failure; void updateProfile( diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index c73918ef..12cf7d4c 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -505,14 +505,14 @@ public class DbusSignalImpl implements Signal { final String aboutEmoji, String avatarPath, final boolean removeAvatar - ) { + ) { try { if (avatarPath.isEmpty()) { avatarPath = null; } Optional avatarFile = removeAvatar ? Optional.absent() - : avatarPath == null ? null : Optional.of(new File(avatarPath)); + : avatarPath == null ? null : Optional.of(new File(avatarPath)); m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile); } catch (IOException e) { throw new Error.Failure(e.getMessage()); From 7c9fd9d0fb7b303e8194a6de9aed852c488afc25 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 21:11:53 +0200 Subject: [PATCH 12/26] Refactor NoteToSelf to singleton class --- .../asamk/signal/manager/api/RecipientIdentifier.java | 9 ++------- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 2 +- src/main/java/org/asamk/signal/util/CommandUtil.java | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index 4a66cbb3..cb0a08bb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -12,14 +12,9 @@ public abstract class RecipientIdentifier { public static class NoteToSelf extends RecipientIdentifier { - @Override - public boolean equals(final Object obj) { - return obj instanceof NoteToSelf; - } + public static NoteToSelf INSTANCE = new NoteToSelf(); - @Override - public int hashCode() { - return 5; + private NoteToSelf() { } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 12cf7d4c..e975a671 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -276,7 +276,7 @@ public class DbusSignalImpl implements Signal { ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { final var results = m.sendMessage(new Message(message, attachments), - Set.of(new RecipientIdentifier.NoteToSelf())); + Set.of(RecipientIdentifier.NoteToSelf.INSTANCE)); checkSendMessageResults(results.getTimestamp(), results.getResults()); return results.getTimestamp(); } catch (AttachmentInvalidException e) { diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java index 83674876..18b38a2a 100644 --- a/src/main/java/org/asamk/signal/util/CommandUtil.java +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -25,7 +25,7 @@ public class CommandUtil { ) throws UserErrorException { final var recipientIdentifiers = new HashSet(); if (isNoteToSelf) { - recipientIdentifiers.add(new RecipientIdentifier.NoteToSelf()); + recipientIdentifiers.add(RecipientIdentifier.NoteToSelf.INSTANCE); } if (recipientStrings != null) { final var localNumber = m.getUsername(); From 1a81bbecbb1d40ef08ab6b3b1913dfe73c678262 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 21:12:37 +0200 Subject: [PATCH 13/26] Do not send message resend request to own device --- .../asamk/signal/manager/helper/IncomingMessageHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 0917a214..45173da4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -144,7 +144,8 @@ public final class IncomingMessageHandler { final var sender = account.getRecipientStore().resolveRecipient(e.getSender()); final var senderProfile = profileProvider.getProfile(sender); final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId()); - if (senderProfile != null + if (e.getSenderDevice() != account.getDeviceId() + && senderProfile != null && senderProfile.getCapabilities().contains(Profile.Capability.senderKey) && selfProfile != null && selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) { From b91c162159c7c28d049ceb8889c419791573d3bb Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 15 Sep 2021 21:40:47 +0200 Subject: [PATCH 14/26] Extract Manager interface --- .../org/asamk/signal/manager/Manager.java | 1134 ++------------- .../org/asamk/signal/manager/ManagerImpl.java | 1240 +++++++++++++++++ .../signal/manager/ProvisioningManager.java | 6 +- .../signal/manager/RegistrationManager.java | 4 +- 4 files changed, 1324 insertions(+), 1060 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/ManagerImpl.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 05700379..d2eb0f8f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,22 +1,5 @@ -/* - Copyright (C) 2015-2021 AsamK and contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ package org.asamk.signal.manager; -import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -25,7 +8,6 @@ import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; -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; @@ -34,233 +16,46 @@ 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; -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.IncomingMessageHandler; -import org.asamk.signal.manager.helper.PinHelper; -import org.asamk.signal.manager.helper.PreKeyHelper; -import org.asamk.signal.manager.helper.ProfileHelper; -import org.asamk.signal.manager.helper.SendHelper; -import org.asamk.signal.manager.helper.StorageHelper; -import org.asamk.signal.manager.helper.SyncHelper; -import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; -import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; -import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; -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; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceContent; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.signalservice.api.util.DeviceNameUtil; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; -import org.whispersystems.signalservice.internal.contacts.crypto.Quote; -import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; -import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; -import org.whispersystems.signalservice.internal.util.Hex; -import org.whispersystems.signalservice.internal.util.Util; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.net.URI; -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; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutorService; -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; +public interface Manager extends Closeable { -public class Manager implements Closeable { - - private final static Logger logger = LoggerFactory.getLogger(Manager.class); - - private final ServiceEnvironmentConfig serviceEnvironmentConfig; - private final SignalDependencies dependencies; - - private SignalAccount account; - - private final ExecutorService executor = Executors.newCachedThreadPool(); - - private final ProfileHelper profileHelper; - private final PinHelper pinHelper; - private final StorageHelper storageHelper; - private final SendHelper sendHelper; - private final SyncHelper syncHelper; - private final AttachmentHelper attachmentHelper; - private final GroupHelper groupHelper; - private final ContactHelper contactHelper; - private final IncomingMessageHandler incomingMessageHandler; - private final PreKeyHelper preKeyHelper; - - private final Context context; - private boolean hasCaughtUpWithOldMessages = false; - - Manager( - SignalAccount account, - PathConfig pathConfig, - ServiceEnvironmentConfig serviceEnvironmentConfig, - String userAgent - ) { - this.account = account; - this.serviceEnvironmentConfig = serviceEnvironmentConfig; - - final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(), - account.getUsername(), - account.getPassword(), - account.getDeviceId()); - final var sessionLock = new SignalSessionLock() { - private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); - - @Override - public Lock acquire() { - LEGACY_LOCK.lock(); - return LEGACY_LOCK::unlock; - } - }; - this.dependencies = new SignalDependencies(serviceEnvironmentConfig, - userAgent, - credentialsProvider, - account.getSignalProtocolStore(), - executor, - sessionLock); - final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); - - 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); - this.profileHelper = new ProfileHelper(account, - dependencies, - avatarStore, - account.getProfileStore()::getProfileKey, - unidentifiedAccessHelper::getAccessFor, - this::resolveSignalServiceAddress); - final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, - this::getRecipientProfile, - account::getSelfRecipientId, - dependencies.getGroupsV2Operations(), - dependencies.getGroupsV2Api(), - this::resolveSignalServiceAddress); - this.sendHelper = new SendHelper(account, - dependencies, - unidentifiedAccessHelper, - this::resolveSignalServiceAddress, - account.getRecipientStore(), - this::handleIdentityFailure, - this::getGroup, - this::refreshRegisteredUser); - this.groupHelper = new GroupHelper(account, - dependencies, - attachmentHelper, - sendHelper, - groupV2Helper, - avatarStore, - this::resolveSignalServiceAddress, - account.getRecipientStore()); - this.storageHelper = new StorageHelper(account, dependencies, groupHelper); - this.contactHelper = new ContactHelper(account); - this.syncHelper = new SyncHelper(account, - attachmentHelper, - sendHelper, - groupHelper, - avatarStore, - this::resolveSignalServiceAddress); - preKeyHelper = new PreKeyHelper(account, dependencies); - - this.context = new Context(account, - dependencies, - stickerPackStore, - sendHelper, - groupHelper, - syncHelper, - profileHelper, - storageHelper, - preKeyHelper); - var jobExecutor = new JobExecutor(context); - - this.incomingMessageHandler = new IncomingMessageHandler(account, - dependencies, - account.getRecipientStore(), - this::resolveSignalServiceAddress, - groupHelper, - contactHelper, - attachmentHelper, - syncHelper, - this::getRecipientProfile, - jobExecutor); - } - - public String getUsername() { - return account.getUsername(); - } - - public RecipientId getSelfRecipientId() { - return account.getSelfRecipientId(); - } - - public int getDeviceId() { - return account.getDeviceId(); - } - - public static Manager init( + static Manager init( String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent, - final TrustNewIdentity trustNewIdentity + TrustNewIdentity trustNewIdentity ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); @@ -276,10 +71,10 @@ public class Manager implements Closeable { final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent); - return new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); } - public static List getAllLocalUsernames(File settingsPath) { + static List getAllLocalUsernames(File settingsPath) { var pathConfig = PathConfig.createDefault(settingsPath); final var dataPath = pathConfig.getDataPath(); final var files = dataPath.listFiles(); @@ -295,208 +90,51 @@ public class Manager implements Closeable { .collect(Collectors.toList()); } - public void checkAccountState() throws IOException { - if (account.getLastReceiveTimestamp() == 0) { - logger.info("The Signal protocol expects that incoming messages are regularly received."); - } else { - var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); - long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); - if (days > 7) { - logger.warn( - "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.", - days); - } - } - preKeyHelper.refreshPreKeysIfNecessary(); - if (account.getUuid() == null) { - account.setUuid(dependencies.getAccountManager().getOwnUuid()); - } - updateAccountAttributes(null); - } + String getUsername(); - /** - * This is used for checking a set of phone numbers for registration on Signal - * - * @param numbers The set of phone number in question - * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. - * @throws IOException if its unable to get the contacts to check if they're registered - */ - public Map> areUsersRegistered(Set numbers) throws IOException { - Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { - try { - return PhoneNumberFormatter.formatNumber(n, account.getUsername()); - } catch (InvalidNumberException e) { - return ""; - } - })); + RecipientId getSelfRecipientId(); - // Note "registeredUsers" has no optionals. It only gives us info on users who are registered - var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values() - .stream() - .filter(s -> !s.isEmpty()) - .collect(Collectors.toSet())); + int getDeviceId(); - return numbers.stream().collect(Collectors.toMap(n -> n, n -> { - final var number = canonicalizedNumbers.get(n); - final var uuid = registeredUsers.get(number); - return new Pair<>(number.isEmpty() ? null : number, uuid); - })); - } + void checkAccountState() throws IOException; - public void updateAccountAttributes(String deviceName) throws IOException { - final String encryptedDeviceName; - if (deviceName == null) { - encryptedDeviceName = account.getEncryptedDeviceName(); - } else { - final var privateKey = account.getIdentityKeyPair().getPrivateKey(); - encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); - account.setEncryptedDeviceName(encryptedDeviceName); - } - dependencies.getAccountManager() - .setAccountAttributes(encryptedDeviceName, - null, - account.getLocalRegistrationId(), - true, - null, - account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - capabilities, - account.isDiscoverableByPhoneNumber()); - } + Map> areUsersRegistered(Set numbers) throws IOException; - /** - * @param givenName if null, the previous givenName will be kept - * @param familyName if null, the previous familyName will be kept - * @param about if null, the previous about text will be kept - * @param aboutEmoji if null, the previous about emoji will be kept - * @param avatar if avatar is null the image from the local avatar store is used (if present), - */ - public void setProfile( - String givenName, final String familyName, String about, String aboutEmoji, Optional avatar - ) throws IOException { - profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); - syncHelper.sendSyncFetchProfileMessage(); - } + void updateAccountAttributes(String deviceName) throws IOException; - public void unregister() throws IOException { - // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. - // If this is the master device, other users can't send messages to this number anymore. - // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. - dependencies.getAccountManager().setGcmId(Optional.absent()); + void setProfile( + String givenName, String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException; - account.setRegistered(false); - } + void unregister() throws IOException; - public void deleteAccount() throws IOException { - try { - pinHelper.removeRegistrationLockPin(); - } catch (UnauthenticatedResponseException e) { - logger.warn("Failed to remove registration lock pin"); - } - account.setRegistrationLockPin(null, null); + void deleteAccount() throws IOException; - dependencies.getAccountManager().deleteAccount(); + void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException; - account.setRegistered(false); - } + List getLinkedDevices() throws IOException; - public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { - dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); - } + void removeLinkedDevices(int deviceId) throws IOException; - public List getLinkedDevices() throws IOException { - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); - var identityKey = account.getIdentityKeyPair().getPrivateKey(); - return devices.stream().map(d -> { - String deviceName = d.getName(); - if (deviceName != null) { - try { - deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey); - } catch (IOException e) { - logger.debug("Failed to decrypt device name, maybe plain text?", e); - } - } - return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); - }).collect(Collectors.toList()); - } + void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException; - public void removeLinkedDevices(int deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); - } + void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException; - public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + Profile getRecipientProfile(RecipientId recipientId); - addDevice(info.deviceIdentifier, info.deviceKey); - } + List getGroups(); - private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - var identityKeyPair = account.getIdentityKeyPair(); - var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); - - dependencies.getAccountManager() - .addDevice(deviceIdentifier, - deviceKey, - identityKeyPair, - Optional.of(account.getProfileKey().serialize()), - verificationCode); - account.setMultiDevice(true); - } - - public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { - if (!account.isMasterDevice()) { - throw new RuntimeException("Only master device can set a PIN"); - } - if (pin.isPresent()) { - final var masterKey = account.getPinMasterKey() != null - ? account.getPinMasterKey() - : KeyUtils.createMasterKey(); - - pinHelper.setRegistrationLockPin(pin.get(), masterKey); - - account.setRegistrationLockPin(pin.get(), masterKey); - } else { - // Remove KBS Pin - pinHelper.removeRegistrationLockPin(); - - account.setRegistrationLockPin(null, null); - } - } - - void refreshPreKeys() throws IOException { - preKeyHelper.refreshPreKeys(); - } - - public Profile getRecipientProfile(RecipientId recipientId) { - return profileHelper.getRecipientProfile(recipientId); - } - - public List getGroups() { - return account.getGroupStore().getGroups(); - } - - public SendGroupMessageResults quitGroup( + SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins - ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - final var newAdmins = resolveRecipients(groupAdmins); - return groupHelper.quitGroup(groupId, newAdmins); - } + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException; - public void deleteGroup(GroupId groupId) throws IOException { - groupHelper.deleteGroup(groupId); - } + void deleteGroup(GroupId groupId) throws IOException; - public Pair createGroup( + Pair createGroup( String name, Set members, File avatarFile - ) throws IOException, AttachmentInvalidException { - return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); - } + ) throws IOException, AttachmentInvalidException; - public SendGroupMessageResults updateGroup( + SendGroupMessageResults updateGroup( GroupId groupId, String name, String description, @@ -511,724 +149,110 @@ public class Manager implements Closeable { File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup - ) 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); - } + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException; - public Pair joinGroup( + Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl - ) throws IOException, GroupLinkNotActiveException { - return groupHelper.joinGroup(inviteLinkUrl); - } + ) throws IOException, GroupLinkNotActiveException; - public SendMessageResults sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - var results = new HashMap>(); - long timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - for (final var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single) { - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - final var result = sendHelper.sendMessage(messageBuilder, recipientId); - results.put(recipient, List.of(result)); - } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { - final var result = sendHelper.sendSelfMessage(messageBuilder); - results.put(recipient, List.of(result)); - } else if (recipient instanceof RecipientIdentifier.Group) { - final var groupId = ((RecipientIdentifier.Group) recipient).groupId; - final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); - results.put(recipient, result); - } - } - return new SendMessageResults(timestamp, results); - } + void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public void sendTypingMessage( - SignalServiceTypingMessage.Action action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - final var timestamp = System.currentTimeMillis(); - for (var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single) { - final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - sendHelper.sendTypingMessage(message, recipientId); - } else if (recipient instanceof RecipientIdentifier.Group) { - final var groupId = ((RecipientIdentifier.Group) recipient).groupId; - final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); - sendHelper.sendGroupTypingMessage(message, groupId); - } - } - } - - public void sendReadReceipt( + void sendReadReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, - messageIds, - System.currentTimeMillis()); + ) throws IOException, UntrustedIdentityException; - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); - } - - public void sendViewedReceipt( + void sendViewedReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, - messageIds, - System.currentTimeMillis()); + ) throws IOException, UntrustedIdentityException; - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); - } - - public SendMessageResults sendMessage( + SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - final var messageBuilder = SignalServiceDataMessage.newBuilder(); - applyMessage(messageBuilder, message); - return sendMessage(messageBuilder, recipients); - } + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - private void applyMessage( - final SignalServiceDataMessage.Builder messageBuilder, final Message message - ) throws AttachmentInvalidException, IOException { - messageBuilder.withBody(message.getMessageText()); - final var attachments = message.getAttachments(); - if (attachments != null) { - messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); - } - } - - public SendMessageResults sendRemoteDeleteMessage( + SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); - return sendMessage(messageBuilder, recipients); - } + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public SendMessageResults sendMessageReaction( + SendMessageResults sendMessageReaction( String emoji, boolean remove, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - var targetAuthorRecipientId = resolveRecipient(targetAuthor); - var reaction = new SignalServiceDataMessage.Reaction(emoji, - remove, - resolveSignalServiceAddress(targetAuthorRecipientId), - targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); - return sendMessage(messageBuilder, recipients); - } + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { - var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); + SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; - try { - return sendMessage(messageBuilder, - recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); - } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { - throw new AssertionError(e); - } finally { - for (var recipient : recipients) { - final var recipientId = resolveRecipient(recipient); - account.getSessionStore().deleteAllSessions(recipientId); - } - } - } - - public void setContactName( + void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException, UnregisteredUserException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); - } - contactHelper.setContactName(resolveRecipient(recipient), name); - } + ) throws NotMasterDeviceException, UnregisteredUserException; - public void setContactBlocked( + void setContactBlocked( RecipientIdentifier.Single recipient, boolean blocked - ) throws NotMasterDeviceException, IOException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); - } - contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); - // TODO cycle our profile key - syncHelper.sendBlockedList(); - } + ) throws NotMasterDeviceException, IOException; - public void setGroupBlocked( - final GroupId groupId, final boolean blocked - ) throws GroupNotFoundException, IOException { - groupHelper.setGroupBlocked(groupId, blocked); - // TODO cycle our profile key - syncHelper.sendBlockedList(); - } + void setGroupBlocked( + GroupId groupId, boolean blocked + ) throws GroupNotFoundException, IOException; - /** - * Change the expiration timer for a contact - */ - public void setExpirationTimer( + void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer - ) throws IOException { - var recipientId = resolveRecipient(recipient); - contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - try { - sendMessage(messageBuilder, Set.of(recipient)); - } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { - throw new AssertionError(e); - } - } + ) throws IOException; - /** - * Upload the sticker pack from path. - * - * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file - * @return if successful, returns the URL to install the sticker pack in the signal app - */ - public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { - var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); + URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException; - var messageSender = dependencies.getMessageSender(); + void requestAllSyncData() throws IOException; - var packKey = KeyUtils.createStickerUploadKey(); - var packIdString = messageSender.uploadStickerManifest(manifest, packKey); - var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString)); - - var sticker = new Sticker(packId, packKey); - account.getStickerStore().updateSticker(sticker); - - try { - return new URI("https", - "signal.art", - "/addstickers/", - "pack_id=" - + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) - + "&pack_key=" - + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); - } catch (URISyntaxException e) { - throw new AssertionError(e); - } - } - - public void requestAllSyncData() throws IOException { - syncHelper.requestAllSyncData(); - retrieveRemoteStorage(); - } - - void retrieveRemoteStorage() throws IOException { - if (account.getStorageKey() != null) { - storageHelper.readDataFromStorage(); - } - } - - 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()) { - return recipientId; - } - final var number = address.getNumber().get(); - final var uuid = getRegisteredUser(number); - return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); - } - - private UUID getRegisteredUser(final String number) throws IOException { - final Map uuidMap; - try { - uuidMap = getRegisteredUsers(Set.of(number)); - } catch (NumberFormatException e) { - throw new UnregisteredUserException(number, e); - } - final var uuid = uuidMap.get(number); - if (uuid == null) { - throw new UnregisteredUserException(number, null); - } - return uuid; - } - - private Map getRegisteredUsers(final Set numbers) throws IOException { - final Map registeredUsers; - try { - registeredUsers = dependencies.getAccountManager() - .getRegisteredUsers(ServiceConfig.getIasKeyStore(), - numbers, - serviceEnvironmentConfig.getCdsMrenclave()); - } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { - throw new IOException(e); - } - - // Store numbers as recipients so we have the number/uuid association - registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - - return registeredUsers; - } - - public void sendTypingMessage( - TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - sendTypingMessage(action.toSignalService(), recipients); - } - - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { - Set queuedActions = new HashSet<>(); - for (var cachedMessage : account.getMessageCache().getCachedMessages()) { - var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); - if (actions != null) { - queuedActions.addAll(actions); - } - } - handleQueuedActions(queuedActions); - } - - private List retryFailedReceivedMessage( - final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage - ) { - var envelope = cachedMessage.loadEnvelope(); - if (envelope == null) { - cachedMessage.delete(); - return null; - } - - final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); - final var actions = result.first(); - final var exception = result.second(); - - if (exception instanceof UntrustedIdentityException) { - if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { - // Envelope is more than a month old, cleaning up. - cachedMessage.delete(); - return null; - } - if (!envelope.hasSourceUuid()) { - final var identifier = ((UntrustedIdentityException) exception).getSender(); - final var recipientId = account.getRecipientStore().resolveRecipient(identifier); - try { - account.getMessageCache().replaceSender(cachedMessage, recipientId); - } catch (IOException ioException) { - logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); - } - } - return null; - } - - // If successful and for all other errors that are not recoverable, delete the cached message - cachedMessage.delete(); - return actions; - } - - public void receiveMessages( + void receiveMessages( long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler - ) throws IOException { - retryFailedReceivedMessages(handler, ignoreAttachments); + ) throws IOException; - Set queuedActions = new HashSet<>(); + boolean hasCaughtUpWithOldMessages(); - final var signalWebSocket = dependencies.getSignalWebSocket(); - signalWebSocket.connect(); + boolean isContactBlocked(RecipientIdentifier.Single recipient); - hasCaughtUpWithOldMessages = false; + File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId); - while (!Thread.interrupted()) { - SignalServiceEnvelope envelope; - final CachedMessage[] cachedMessage = {null}; - account.setLastReceiveTimestamp(System.currentTimeMillis()); - logger.debug("Checking for new message from server"); - try { - var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { - final var recipientId = envelope1.hasSourceUuid() - ? resolveRecipient(envelope1.getSourceAddress()) - : null; - // store message on disk, before acknowledging receipt to the server - cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); - }); - if (result.isPresent()) { - envelope = result.get(); - logger.debug("New message received from server"); - } else { - logger.debug("Received indicator that server queue is empty"); - handleQueuedActions(queuedActions); - queuedActions.clear(); + void sendContacts() throws IOException; - hasCaughtUpWithOldMessages = true; - synchronized (this) { - this.notifyAll(); - } + List> getContacts(); - // Continue to wait another timeout for new messages - continue; - } - } catch (AssertionError e) { - if (e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - break; - } else { - throw e; - } - } catch (WebSocketUnavailableException e) { - logger.debug("Pipe unexpectedly unavailable, connecting"); - signalWebSocket.connect(); - continue; - } catch (TimeoutException e) { - if (returnOnTimeout) return; - continue; - } + String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier); - final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); - queuedActions.addAll(result.first()); - final var exception = result.second(); + GroupInfo getGroup(GroupId groupId); - if (hasCaughtUpWithOldMessages) { - handleQueuedActions(queuedActions); - } - if (cachedMessage[0] != null) { - if (exception instanceof UntrustedIdentityException) { - final var address = ((UntrustedIdentityException) exception).getSender(); - final var recipientId = resolveRecipient(address); - if (!envelope.hasSourceUuid()) { - try { - cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); - } catch (IOException ioException) { - logger.warn("Failed to move cached message to recipient folder: {}", - ioException.getMessage()); - } - } - } else { - cachedMessage[0].delete(); - } - } - } - handleQueuedActions(queuedActions); - } + List getIdentities(); - public boolean hasCaughtUpWithOldMessages() { - return hasCaughtUpWithOldMessages; - } + List getIdentities(RecipientIdentifier.Single recipient); - private void handleQueuedActions(final Collection queuedActions) { - var interrupted = false; - for (var action : queuedActions) { - try { - action.execute(context); - } catch (Throwable e) { - if ((e instanceof AssertionError || e instanceof RuntimeException) - && e.getCause() instanceof InterruptedException) { - interrupted = true; - continue; - } - logger.warn("Message action failed.", e); - } - } - if (interrupted) { - Thread.currentThread().interrupt(); - } - } + boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint); - public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { - final RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - return contactHelper.isContactBlocked(recipientId); - } + boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber); - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return attachmentHelper.getAttachmentFile(attachmentId); - } + boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber); - public void sendContacts() throws IOException { - syncHelper.sendContacts(); - } + boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient); - public List> getContacts() { - return account.getContactStore().getContacts(); - } + String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { - final RecipientId recipientId; - try { - recipientId = resolveRecipient(recipientIdentifier); - } catch (UnregisteredUserException e) { - return null; - } + byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - final var contact = account.getContactStore().getContact(recipientId); - if (contact != null && !Util.isEmpty(contact.getName())) { - return contact.getName(); - } + SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); - final var profile = getRecipientProfile(recipientId); - if (profile != null) { - return profile.getDisplayName(); - } + SignalServiceAddress resolveSignalServiceAddress(UUID uuid); - return null; - } - - public GroupInfo getGroup(GroupId groupId) { - return groupHelper.getGroup(groupId); - } - - public List getIdentities() { - return account.getIdentityKeyStore().getIdentities(); - } - - public List getIdentities(RecipientIdentifier.Single recipient) { - IdentityInfo identity; - try { - identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); - } catch (UnregisteredUserException e) { - identity = null; - } - return identity == null ? List.of() : List.of(identity); - } - - /** - * Trust this the identity with this fingerprint - * - * @param recipient username of the identity - * @param fingerprint Fingerprint - */ - public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - return trustIdentity(recipientId, - identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), - TrustLevel.TRUSTED_VERIFIED); - } - - /** - * Trust this the identity with this safety number - * - * @param recipient username of the identity - * @param safetyNumber Safety number - */ - public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, - identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), - TrustLevel.TRUSTED_VERIFIED); - } - - /** - * Trust this the identity with this scannable safety number - * - * @param recipient username of the identity - * @param safetyNumber Scannable safety number - */ - public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } 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); - } - - /** - * Trust all keys of this identity without verification - * - * @param recipient username of the identity - */ - public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } 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; - } - - 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); - } - } - - public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); - } - - public 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); - } - - public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { - return resolveSignalServiceAddress(resolveRecipient(address)); - } - - public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); - } - - public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { - final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); - if (address.getUuid().isPresent()) { - return address.toSignalServiceAddress(); - } - - // Address in recipient store doesn't have a uuid, this shouldn't happen - // Try to retrieve the uuid from the server - final var number = address.getNumber().get(); - try { - return resolveSignalServiceAddress(getRegisteredUser(number)); - } catch (IOException e) { - logger.warn("Failed to get uuid for e164 number: {}", number, e); - // Return SignalServiceAddress with unknown UUID - return address.toSignalServiceAddress(); - } - } - - private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { - final var recipientIds = new HashSet(recipients.size()); - for (var number : recipients) { - final var recipientId = resolveRecipient(number); - recipientIds.add(recipientId); - } - return recipientIds; - } - - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { - if (recipient instanceof RecipientIdentifier.Uuid) { - return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); - } else { - final var number = ((RecipientIdentifier.Number) recipient).number; - return account.getRecipientStore().resolveRecipient(number, () -> { - try { - return getRegisteredUser(number); - } catch (IOException e) { - return null; - } - }); - } - } - - private RecipientId resolveRecipient(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipient(address); - } - - private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipientTrusted(address); - } + SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId); @Override - public void close() throws IOException { - close(true); - } + void close() throws IOException; - private void close(boolean closeAccount) throws IOException { - executor.shutdown(); - - dependencies.getSignalWebSocket().disconnect(); - - if (closeAccount && account != null) { - account.close(); - } - account = null; - } - - public interface ReceiveMessageHandler { + interface ReceiveMessageHandler { void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); } diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java new file mode 100644 index 00000000..d0fab350 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -0,0 +1,1240 @@ +/* + Copyright (C) 2015-2021 AsamK and contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package org.asamk.signal.manager; + +import org.asamk.signal.manager.actions.HandleAction; +import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Message; +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.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; +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.IncomingMessageHandler; +import org.asamk.signal.manager.helper.PinHelper; +import org.asamk.signal.manager.helper.PreKeyHelper; +import org.asamk.signal.manager.helper.ProfileHelper; +import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.StorageHelper; +import org.asamk.signal.manager.helper.SyncHelper; +import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.storage.messageCache.CachedMessage; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientId; +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; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.DeviceNameUtil; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; +import org.whispersystems.signalservice.internal.contacts.crypto.Quote; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; +import org.whispersystems.signalservice.internal.util.Hex; +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +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; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +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; + +public class ManagerImpl implements Manager { + + private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); + + private final ServiceEnvironmentConfig serviceEnvironmentConfig; + private final SignalDependencies dependencies; + + private SignalAccount account; + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + private final ProfileHelper profileHelper; + private final PinHelper pinHelper; + private final StorageHelper storageHelper; + private final SendHelper sendHelper; + private final SyncHelper syncHelper; + private final AttachmentHelper attachmentHelper; + private final GroupHelper groupHelper; + private final ContactHelper contactHelper; + private final IncomingMessageHandler incomingMessageHandler; + private final PreKeyHelper preKeyHelper; + + private final Context context; + private boolean hasCaughtUpWithOldMessages = false; + + ManagerImpl( + SignalAccount account, + PathConfig pathConfig, + ServiceEnvironmentConfig serviceEnvironmentConfig, + String userAgent + ) { + this.account = account; + this.serviceEnvironmentConfig = serviceEnvironmentConfig; + + final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(), + account.getUsername(), + account.getPassword(), + account.getDeviceId()); + final var sessionLock = new SignalSessionLock() { + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } + }; + this.dependencies = new SignalDependencies(serviceEnvironmentConfig, + userAgent, + credentialsProvider, + account.getSignalProtocolStore(), + executor, + sessionLock); + final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); + final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + + 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); + this.profileHelper = new ProfileHelper(account, + dependencies, + avatarStore, + account.getProfileStore()::getProfileKey, + unidentifiedAccessHelper::getAccessFor, + this::resolveSignalServiceAddress); + final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, + this::getRecipientProfile, + account::getSelfRecipientId, + dependencies.getGroupsV2Operations(), + dependencies.getGroupsV2Api(), + this::resolveSignalServiceAddress); + this.sendHelper = new SendHelper(account, + dependencies, + unidentifiedAccessHelper, + this::resolveSignalServiceAddress, + account.getRecipientStore(), + this::handleIdentityFailure, + this::getGroup, + this::refreshRegisteredUser); + this.groupHelper = new GroupHelper(account, + dependencies, + attachmentHelper, + sendHelper, + groupV2Helper, + avatarStore, + this::resolveSignalServiceAddress, + account.getRecipientStore()); + this.storageHelper = new StorageHelper(account, dependencies, groupHelper); + this.contactHelper = new ContactHelper(account); + this.syncHelper = new SyncHelper(account, + attachmentHelper, + sendHelper, + groupHelper, + avatarStore, + this::resolveSignalServiceAddress); + preKeyHelper = new PreKeyHelper(account, dependencies); + + this.context = new Context(account, + dependencies, + stickerPackStore, + sendHelper, + groupHelper, + syncHelper, + profileHelper, + storageHelper, + preKeyHelper); + var jobExecutor = new JobExecutor(context); + + this.incomingMessageHandler = new IncomingMessageHandler(account, + dependencies, + account.getRecipientStore(), + this::resolveSignalServiceAddress, + groupHelper, + contactHelper, + attachmentHelper, + syncHelper, + this::getRecipientProfile, + jobExecutor); + } + + @Override + public String getUsername() { + return account.getUsername(); + } + + @Override + public RecipientId getSelfRecipientId() { + return account.getSelfRecipientId(); + } + + @Override + public int getDeviceId() { + return account.getDeviceId(); + } + + @Override + public void checkAccountState() throws IOException { + if (account.getLastReceiveTimestamp() == 0) { + logger.info("The Signal protocol expects that incoming messages are regularly received."); + } else { + var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); + long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); + if (days > 7) { + logger.warn( + "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.", + days); + } + } + preKeyHelper.refreshPreKeysIfNecessary(); + if (account.getUuid() == null) { + account.setUuid(dependencies.getAccountManager().getOwnUuid()); + } + updateAccountAttributes(null); + } + + /** + * This is used for checking a set of phone numbers for registration on Signal + * + * @param numbers The set of phone number in question + * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. + * @throws IOException if its unable to get the contacts to check if they're registered + */ + @Override + public Map> areUsersRegistered(Set numbers) throws IOException { + Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { + try { + return PhoneNumberFormatter.formatNumber(n, account.getUsername()); + } catch (InvalidNumberException e) { + return ""; + } + })); + + // Note "registeredUsers" has no optionals. It only gives us info on users who are registered + var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values() + .stream() + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet())); + + return numbers.stream().collect(Collectors.toMap(n -> n, n -> { + final var number = canonicalizedNumbers.get(n); + final var uuid = registeredUsers.get(number); + return new Pair<>(number.isEmpty() ? null : number, uuid); + })); + } + + @Override + public void updateAccountAttributes(String deviceName) throws IOException { + final String encryptedDeviceName; + if (deviceName == null) { + encryptedDeviceName = account.getEncryptedDeviceName(); + } else { + final var privateKey = account.getIdentityKeyPair().getPrivateKey(); + encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); + account.setEncryptedDeviceName(encryptedDeviceName); + } + dependencies.getAccountManager() + .setAccountAttributes(encryptedDeviceName, + null, + account.getLocalRegistrationId(), + true, + null, + account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + capabilities, + account.isDiscoverableByPhoneNumber()); + } + + /** + * @param givenName if null, the previous givenName will be kept + * @param familyName if null, the previous familyName will be kept + * @param about if null, the previous about text will be kept + * @param aboutEmoji if null, the previous about emoji will be kept + * @param avatar if avatar is null the image from the local avatar store is used (if present), + */ + @Override + public void setProfile( + String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException { + profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); + syncHelper.sendSyncFetchProfileMessage(); + } + + @Override + public void unregister() throws IOException { + // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. + // If this is the master device, other users can't send messages to this number anymore. + // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. + dependencies.getAccountManager().setGcmId(Optional.absent()); + + account.setRegistered(false); + } + + @Override + public void deleteAccount() throws IOException { + try { + pinHelper.removeRegistrationLockPin(); + } catch (UnauthenticatedResponseException e) { + logger.warn("Failed to remove registration lock pin"); + } + account.setRegistrationLockPin(null, null); + + dependencies.getAccountManager().deleteAccount(); + + account.setRegistered(false); + } + + @Override + public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { + dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); + } + + @Override + public List getLinkedDevices() throws IOException { + var devices = dependencies.getAccountManager().getDevices(); + account.setMultiDevice(devices.size() > 1); + var identityKey = account.getIdentityKeyPair().getPrivateKey(); + return devices.stream().map(d -> { + String deviceName = d.getName(); + if (deviceName != null) { + try { + deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey); + } catch (IOException e) { + logger.debug("Failed to decrypt device name, maybe plain text?", e); + } + } + return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); + }).collect(Collectors.toList()); + } + + @Override + public void removeLinkedDevices(int deviceId) throws IOException { + dependencies.getAccountManager().removeDevice(deviceId); + var devices = dependencies.getAccountManager().getDevices(); + account.setMultiDevice(devices.size() > 1); + } + + @Override + public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { + var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + + addDevice(info.deviceIdentifier, info.deviceKey); + } + + private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + var identityKeyPair = account.getIdentityKeyPair(); + var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); + + dependencies.getAccountManager() + .addDevice(deviceIdentifier, + deviceKey, + identityKeyPair, + Optional.of(account.getProfileKey().serialize()), + verificationCode); + account.setMultiDevice(true); + } + + @Override + public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { + if (!account.isMasterDevice()) { + throw new RuntimeException("Only master device can set a PIN"); + } + if (pin.isPresent()) { + final var masterKey = account.getPinMasterKey() != null + ? account.getPinMasterKey() + : KeyUtils.createMasterKey(); + + pinHelper.setRegistrationLockPin(pin.get(), masterKey); + + account.setRegistrationLockPin(pin.get(), masterKey); + } else { + // Remove KBS Pin + pinHelper.removeRegistrationLockPin(); + + account.setRegistrationLockPin(null, null); + } + } + + void refreshPreKeys() throws IOException { + preKeyHelper.refreshPreKeys(); + } + + @Override + public Profile getRecipientProfile(RecipientId recipientId) { + return profileHelper.getRecipientProfile(recipientId); + } + + @Override + public List getGroups() { + return account.getGroupStore().getGroups(); + } + + @Override + public SendGroupMessageResults quitGroup( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { + final var newAdmins = resolveRecipients(groupAdmins); + return groupHelper.quitGroup(groupId, newAdmins); + } + + @Override + public void deleteGroup(GroupId groupId) throws IOException { + groupHelper.deleteGroup(groupId); + } + + @Override + public Pair createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); + } + + @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 + ) 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); + } + + @Override + public Pair joinGroup( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + return groupHelper.joinGroup(inviteLinkUrl); + } + + private SendMessageResults sendMessage( + SignalServiceDataMessage.Builder messageBuilder, Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + var results = new HashMap>(); + long timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + final var result = sendHelper.sendMessage(messageBuilder, recipientId); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { + final var result = sendHelper.sendSelfMessage(messageBuilder); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); + results.put(recipient, result); + } + } + return new SendMessageResults(timestamp, results); + } + + private void sendTypingMessage( + SignalServiceTypingMessage.Action action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + final var timestamp = System.currentTimeMillis(); + for (var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + sendHelper.sendTypingMessage(message, recipientId); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); + sendHelper.sendGroupTypingMessage(message, groupId); + } + } + } + + @Override + public void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + sendTypingMessage(action.toSignalService(), recipients); + } + + @Override + public void sendReadReceipt( + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, + messageIds, + System.currentTimeMillis()); + + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + } + + @Override + public void sendViewedReceipt( + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, + messageIds, + System.currentTimeMillis()); + + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + } + + @Override + public SendMessageResults sendMessage( + Message message, Set recipients + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + final var messageBuilder = SignalServiceDataMessage.newBuilder(); + applyMessage(messageBuilder, message); + return sendMessage(messageBuilder, recipients); + } + + private void applyMessage( + final SignalServiceDataMessage.Builder messageBuilder, final Message message + ) throws AttachmentInvalidException, IOException { + messageBuilder.withBody(message.getMessageText()); + final var attachments = message.getAttachments(); + if (attachments != null) { + messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); + } + } + + @Override + public SendMessageResults sendRemoteDeleteMessage( + long targetSentTimestamp, Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); + return sendMessage(messageBuilder, recipients); + } + + @Override + public SendMessageResults sendMessageReaction( + String emoji, + boolean remove, + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + var targetAuthorRecipientId = resolveRecipient(targetAuthor); + var reaction = new SignalServiceDataMessage.Reaction(emoji, + remove, + resolveSignalServiceAddress(targetAuthorRecipientId), + targetSentTimestamp); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); + return sendMessage(messageBuilder, recipients); + } + + @Override + public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { + var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); + + try { + return sendMessage(messageBuilder, + recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new AssertionError(e); + } finally { + for (var recipient : recipients) { + final var recipientId = resolveRecipient(recipient); + account.getSessionStore().deleteAllSessions(recipientId); + } + } + } + + @Override + public void setContactName( + RecipientIdentifier.Single recipient, String name + ) throws NotMasterDeviceException, UnregisteredUserException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + contactHelper.setContactName(resolveRecipient(recipient), name); + } + + @Override + public void setContactBlocked( + RecipientIdentifier.Single recipient, boolean blocked + ) throws NotMasterDeviceException, IOException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); + // TODO cycle our profile key + syncHelper.sendBlockedList(); + } + + @Override + public void setGroupBlocked( + final GroupId groupId, final boolean blocked + ) throws GroupNotFoundException, IOException { + groupHelper.setGroupBlocked(groupId, blocked); + // TODO cycle our profile key + syncHelper.sendBlockedList(); + } + + /** + * Change the expiration timer for a contact + */ + @Override + public void setExpirationTimer( + RecipientIdentifier.Single recipient, int messageExpirationTimer + ) throws IOException { + var recipientId = resolveRecipient(recipient); + contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + try { + sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { + throw new AssertionError(e); + } + } + + /** + * Upload the sticker pack from path. + * + * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file + * @return if successful, returns the URL to install the sticker pack in the signal app + */ + @Override + public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { + var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); + + var messageSender = dependencies.getMessageSender(); + + var packKey = KeyUtils.createStickerUploadKey(); + var packIdString = messageSender.uploadStickerManifest(manifest, packKey); + var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString)); + + var sticker = new Sticker(packId, packKey); + account.getStickerStore().updateSticker(sticker); + + try { + return new URI("https", + "signal.art", + "/addstickers/", + "pack_id=" + + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + + "&pack_key=" + + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + @Override + public void requestAllSyncData() throws IOException { + syncHelper.requestAllSyncData(); + retrieveRemoteStorage(); + } + + void retrieveRemoteStorage() throws IOException { + if (account.getStorageKey() != null) { + storageHelper.readDataFromStorage(); + } + } + + 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()) { + return recipientId; + } + final var number = address.getNumber().get(); + final var uuid = getRegisteredUser(number); + return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); + } + + private UUID getRegisteredUser(final String number) throws IOException { + final Map uuidMap; + try { + uuidMap = getRegisteredUsers(Set.of(number)); + } catch (NumberFormatException e) { + throw new UnregisteredUserException(number, e); + } + final var uuid = uuidMap.get(number); + if (uuid == null) { + throw new UnregisteredUserException(number, null); + } + return uuid; + } + + private Map getRegisteredUsers(final Set numbers) throws IOException { + final Map registeredUsers; + try { + registeredUsers = dependencies.getAccountManager() + .getRegisteredUsers(ServiceConfig.getIasKeyStore(), + numbers, + serviceEnvironmentConfig.getCdsMrenclave()); + } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { + throw new IOException(e); + } + + // Store numbers as recipients so we have the number/uuid association + registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); + + return registeredUsers; + } + + private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { + Set queuedActions = new HashSet<>(); + for (var cachedMessage : account.getMessageCache().getCachedMessages()) { + var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); + if (actions != null) { + queuedActions.addAll(actions); + } + } + handleQueuedActions(queuedActions); + } + + private List retryFailedReceivedMessage( + final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage + ) { + var envelope = cachedMessage.loadEnvelope(); + if (envelope == null) { + cachedMessage.delete(); + return null; + } + + final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); + final var actions = result.first(); + final var exception = result.second(); + + if (exception instanceof UntrustedIdentityException) { + if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { + // Envelope is more than a month old, cleaning up. + cachedMessage.delete(); + return null; + } + if (!envelope.hasSourceUuid()) { + final var identifier = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = account.getRecipientStore().resolveRecipient(identifier); + try { + account.getMessageCache().replaceSender(cachedMessage, recipientId); + } catch (IOException ioException) { + logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); + } + } + return null; + } + + // If successful and for all other errors that are not recoverable, delete the cached message + cachedMessage.delete(); + return actions; + } + + @Override + public void receiveMessages( + long timeout, + TimeUnit unit, + boolean returnOnTimeout, + boolean ignoreAttachments, + ReceiveMessageHandler handler + ) throws IOException { + retryFailedReceivedMessages(handler, ignoreAttachments); + + Set queuedActions = new HashSet<>(); + + final var signalWebSocket = dependencies.getSignalWebSocket(); + signalWebSocket.connect(); + + hasCaughtUpWithOldMessages = false; + + while (!Thread.interrupted()) { + SignalServiceEnvelope envelope; + final CachedMessage[] cachedMessage = {null}; + account.setLastReceiveTimestamp(System.currentTimeMillis()); + logger.debug("Checking for new message from server"); + try { + var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { + final var recipientId = envelope1.hasSourceUuid() + ? resolveRecipient(envelope1.getSourceAddress()) + : null; + // store message on disk, before acknowledging receipt to the server + cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); + }); + if (result.isPresent()) { + envelope = result.get(); + logger.debug("New message received from server"); + } else { + logger.debug("Received indicator that server queue is empty"); + handleQueuedActions(queuedActions); + queuedActions.clear(); + + hasCaughtUpWithOldMessages = true; + synchronized (this) { + this.notifyAll(); + } + + // Continue to wait another timeout for new messages + continue; + } + } catch (AssertionError e) { + if (e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + break; + } else { + throw e; + } + } catch (WebSocketUnavailableException e) { + logger.debug("Pipe unexpectedly unavailable, connecting"); + signalWebSocket.connect(); + continue; + } catch (TimeoutException e) { + if (returnOnTimeout) return; + continue; + } + + final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); + queuedActions.addAll(result.first()); + final var exception = result.second(); + + if (hasCaughtUpWithOldMessages) { + handleQueuedActions(queuedActions); + } + if (cachedMessage[0] != null) { + if (exception instanceof UntrustedIdentityException) { + final var address = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = resolveRecipient(address); + if (!envelope.hasSourceUuid()) { + try { + cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); + } catch (IOException ioException) { + logger.warn("Failed to move cached message to recipient folder: {}", + ioException.getMessage()); + } + } + } else { + cachedMessage[0].delete(); + } + } + } + handleQueuedActions(queuedActions); + } + + @Override + public boolean hasCaughtUpWithOldMessages() { + return hasCaughtUpWithOldMessages; + } + + private void handleQueuedActions(final Collection queuedActions) { + var interrupted = false; + for (var action : queuedActions) { + try { + action.execute(context); + } catch (Throwable e) { + if ((e instanceof AssertionError || e instanceof RuntimeException) + && e.getCause() instanceof InterruptedException) { + interrupted = true; + continue; + } + logger.warn("Message action failed.", e); + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + @Override + public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + return contactHelper.isContactBlocked(recipientId); + } + + @Override + public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { + return attachmentHelper.getAttachmentFile(attachmentId); + } + + @Override + public void sendContacts() throws IOException { + syncHelper.sendContacts(); + } + + @Override + public List> getContacts() { + return account.getContactStore().getContacts(); + } + + @Override + public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipientIdentifier); + } catch (UnregisteredUserException e) { + return null; + } + + final var contact = account.getContactStore().getContact(recipientId); + if (contact != null && !Util.isEmpty(contact.getName())) { + return contact.getName(); + } + + final var profile = getRecipientProfile(recipientId); + if (profile != null) { + return profile.getDisplayName(); + } + + return null; + } + + @Override + public GroupInfo getGroup(GroupId groupId) { + return groupHelper.getGroup(groupId); + } + + @Override + public List getIdentities() { + return account.getIdentityKeyStore().getIdentities(); + } + + @Override + public List getIdentities(RecipientIdentifier.Single recipient) { + IdentityInfo identity; + try { + identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + } catch (UnregisteredUserException e) { + identity = null; + } + return identity == null ? List.of() : List.of(identity); + } + + /** + * Trust this the identity with this fingerprint + * + * @param recipient username of the identity + * @param fingerprint Fingerprint + */ + @Override + public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + return trustIdentity(recipientId, + identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), + TrustLevel.TRUSTED_VERIFIED); + } + + /** + * Trust this the identity with this safety number + * + * @param recipient username of the identity + * @param safetyNumber Safety number + */ + @Override + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); + return trustIdentity(recipientId, + identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), + TrustLevel.TRUSTED_VERIFIED); + } + + /** + * Trust this the identity with this scannable safety number + * + * @param recipient username of the identity + * @param safetyNumber Scannable safety number + */ + @Override + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } 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); + } + + /** + * Trust all keys of this identity without verification + * + * @param recipient username of the identity + */ + @Override + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } 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; + } + + 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(); + } + + @Override + public 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); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { + return resolveSignalServiceAddress(resolveRecipient(address)); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); + if (address.getUuid().isPresent()) { + return address.toSignalServiceAddress(); + } + + // Address in recipient store doesn't have a uuid, this shouldn't happen + // Try to retrieve the uuid from the server + final var number = address.getNumber().get(); + try { + return resolveSignalServiceAddress(getRegisteredUser(number)); + } catch (IOException e) { + logger.warn("Failed to get uuid for e164 number: {}", number, e); + // Return SignalServiceAddress with unknown UUID + return address.toSignalServiceAddress(); + } + } + + private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { + final var recipientIds = new HashSet(recipients.size()); + for (var number : recipients) { + final var recipientId = resolveRecipient(number); + recipientIds.add(recipientId); + } + return recipientIds; + } + + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { + if (recipient instanceof RecipientIdentifier.Uuid) { + return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); + } else { + final var number = ((RecipientIdentifier.Number) recipient).number; + return account.getRecipientStore().resolveRecipient(number, () -> { + try { + return getRegisteredUser(number); + } catch (IOException e) { + return null; + } + }); + } + } + + private RecipientId resolveRecipient(SignalServiceAddress address) { + return account.getRecipientStore().resolveRecipient(address); + } + + private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { + return account.getRecipientStore().resolveRecipientTrusted(address); + } + + @Override + public void close() throws IOException { + close(true); + } + + private void close(boolean closeAccount) throws IOException { + executor.shutdown(); + + dependencies.getSignalWebSocket().disconnect(); + + if (closeAccount && account != null) { + account.close(); + } + account = null; + } + +} diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 90dc6c66..226de9be 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -126,9 +126,9 @@ public class ProvisioningManager { profileKey, TrustNewIdentity.ON_FIRST_USE); - Manager m = null; + ManagerImpl m = null; try { - m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); logger.debug("Refreshing pre keys"); try { @@ -178,7 +178,7 @@ public class ProvisioningManager { return false; } - final var m = new Manager(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent); + final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent); try (m) { m.checkAccountState(); } catch (AuthorizationFailedException ignored) { diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 443a7969..978f1fd5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -177,9 +177,9 @@ public class RegistrationManager implements Closeable { //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin); - Manager m = null; + ManagerImpl m = null; try { - m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); account = null; m.refreshPreKeys(); From d72b838560b1a4186ac121c7d605773b49fcdf46 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 18 Sep 2021 10:19:56 +0200 Subject: [PATCH 15/26] Refactor Manager interface --- .../org/asamk/signal/manager/Manager.java | 40 +++--- .../org/asamk/signal/manager/ManagerImpl.java | 123 +++++++++++++----- .../signal/manager/RegistrationManager.java | 8 +- .../signal/manager/UserAlreadyExists.java | 10 +- .../org/asamk/signal/manager/api/Device.java | 8 +- .../org/asamk/signal/manager/api/Group.java | 99 ++++++++++++++ .../asamk/signal/manager/api/Identity.java | 65 +++++++++ .../manager/api/RecipientIdentifier.java | 22 ++++ .../storage/recipients/RecipientAddress.java | 10 ++ src/main/java/org/asamk/Signal.java | 4 +- src/main/java/org/asamk/signal/App.java | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 8 +- .../asamk/signal/commands/BlockCommand.java | 2 +- .../asamk/signal/commands/DaemonCommand.java | 2 +- .../signal/commands/JoinGroupCommand.java | 4 +- .../asamk/signal/commands/LinkCommand.java | 4 +- .../signal/commands/ListContactsCommand.java | 11 +- .../signal/commands/ListDevicesCommand.java | 2 +- .../signal/commands/ListGroupsCommand.java | 49 ++++--- .../commands/ListIdentitiesCommand.java | 18 +-- .../signal/commands/QuitGroupCommand.java | 2 +- .../signal/commands/SendReactionCommand.java | 2 +- .../signal/commands/SendReceiptCommand.java | 2 +- .../signal/commands/SendTypingCommand.java | 2 +- .../asamk/signal/commands/TrustCommand.java | 2 +- .../asamk/signal/commands/UnblockCommand.java | 3 +- .../signal/commands/UpdateContactCommand.java | 2 +- .../signal/commands/UpdateGroupCommand.java | 2 +- .../signal/dbus/DbusSignalControlImpl.java | 2 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 68 +++++----- .../org/asamk/signal/json/JsonMention.java | 3 +- .../signal/json/JsonMessageEnvelope.java | 2 +- .../org/asamk/signal/util/CommandUtil.java | 2 +- 33 files changed, 416 insertions(+), 169 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/Group.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/Identity.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 d2eb0f8f..cba438f8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,6 +1,8 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; @@ -17,12 +19,10 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.SignalAccount; -import org.asamk.signal.manager.storage.groups.GroupInfo; -import org.asamk.signal.manager.storage.identities.IdentityInfo; 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.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; @@ -51,7 +51,7 @@ import java.util.stream.Collectors; public interface Manager extends Closeable { static Manager init( - String username, + String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent, @@ -59,11 +59,11 @@ public interface Manager extends Closeable { ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); - if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { + if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) { throw new NotRegisteredException(); } - var account = SignalAccount.load(pathConfig.getDataPath(), username, true, trustNewIdentity); + var account = SignalAccount.load(pathConfig.getDataPath(), number, true, trustNewIdentity); if (!account.isRegistered()) { throw new NotRegisteredException(); @@ -74,7 +74,7 @@ public interface Manager extends Closeable { return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); } - static List getAllLocalUsernames(File settingsPath) { + static List getAllLocalNumbers(File settingsPath) { var pathConfig = PathConfig.createDefault(settingsPath); final var dataPath = pathConfig.getDataPath(); final var files = dataPath.listFiles(); @@ -90,11 +90,7 @@ public interface Manager extends Closeable { .collect(Collectors.toList()); } - String getUsername(); - - RecipientId getSelfRecipientId(); - - int getDeviceId(); + String getSelfNumber(); void checkAccountState() throws IOException; @@ -120,9 +116,9 @@ public interface Manager extends Closeable { void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException; - Profile getRecipientProfile(RecipientId recipientId); + Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException; - List getGroups(); + List getGroups(); SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins @@ -221,15 +217,15 @@ public interface Manager extends Closeable { void sendContacts() throws IOException; - List> getContacts(); + List> getContacts(); - String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier); + String getContactOrProfileName(RecipientIdentifier.Single recipient); - GroupInfo getGroup(GroupId groupId); + Group getGroup(GroupId groupId); - List getIdentities(); + List getIdentities(); - List getIdentities(RecipientIdentifier.Single recipient); + List getIdentities(RecipientIdentifier.Single recipient); boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint); @@ -241,14 +237,8 @@ public interface Manager extends Closeable { String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); - SignalServiceAddress resolveSignalServiceAddress(UUID uuid); - - SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId); - @Override void close() throws IOException; 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 d0fab350..de60fa50 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -18,6 +18,8 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; @@ -52,6 +54,7 @@ import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.messageCache.CachedMessage; 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.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; @@ -196,7 +199,7 @@ public class ManagerImpl implements Manager { this::resolveSignalServiceAddress, account.getRecipientStore(), this::handleIdentityFailure, - this::getGroup, + this::getGroupInfo, this::refreshRegisteredUser); this.groupHelper = new GroupHelper(account, dependencies, @@ -240,20 +243,10 @@ public class ManagerImpl implements Manager { } @Override - public String getUsername() { + public String getSelfNumber() { return account.getUsername(); } - @Override - public RecipientId getSelfRecipientId() { - return account.getSelfRecipientId(); - } - - @Override - public int getDeviceId() { - return account.getDeviceId(); - } - @Override public void checkAccountState() throws IOException { if (account.getLastReceiveTimestamp() == 0) { @@ -385,7 +378,11 @@ public class ManagerImpl implements Manager { logger.debug("Failed to decrypt device name, maybe plain text?", e); } } - return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); + return new Device(d.getId(), + deviceName, + d.getCreated(), + d.getLastSeen(), + d.getId() == account.getDeviceId()); }).collect(Collectors.toList()); } @@ -442,13 +439,48 @@ public class ManagerImpl implements Manager { } @Override - public Profile getRecipientProfile(RecipientId recipientId) { + public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException { + return profileHelper.getRecipientProfile(resolveRecipient(recipient)); + } + + private Profile getRecipientProfile(RecipientId recipientId) { return profileHelper.getRecipientProfile(recipientId); } @Override - public List getGroups() { - return account.getGroupStore().getGroups(); + public List getGroups() { + return account.getGroupStore().getGroups().stream().map(this::toGroup).collect(Collectors.toList()); + } + + private Group toGroup(final GroupInfo groupInfo) { + if (groupInfo == null) { + return null; + } + + return new Group(groupInfo.getGroupId(), + groupInfo.getTitle(), + groupInfo.getDescription(), + groupInfo.getGroupInviteLink(), + groupInfo.getMembers() + .stream() + .map(account.getRecipientStore()::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getPendingMembers() + .stream() + .map(account.getRecipientStore()::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getRequestingMembers() + .stream() + .map(account.getRecipientStore()::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getAdminMembers() + .stream() + .map(account.getRecipientStore()::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.isBlocked(), + groupInfo.getMessageExpirationTime(), + groupInfo.isAnnouncementGroup(), + groupInfo.isMember(account.getSelfRecipientId())); } @Override @@ -973,15 +1005,19 @@ public class ManagerImpl implements Manager { } @Override - public List> getContacts() { - return account.getContactStore().getContacts(); + public List> getContacts() { + return account.getContactStore() + .getContacts() + .stream() + .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second())) + .collect(Collectors.toList()); } @Override - public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { + public String getContactOrProfileName(RecipientIdentifier.Single recipient) { final RecipientId recipientId; try { - recipientId = resolveRecipient(recipientIdentifier); + recipientId = resolveRecipient(recipient); } catch (UnregisteredUserException e) { return null; } @@ -1000,24 +1036,46 @@ public class ManagerImpl implements Manager { } @Override - public GroupInfo getGroup(GroupId groupId) { + public Group getGroup(GroupId groupId) { + return toGroup(groupHelper.getGroup(groupId)); + } + + public GroupInfo getGroupInfo(GroupId groupId) { return groupHelper.getGroup(groupId); } @Override - public List getIdentities() { - return account.getIdentityKeyStore().getIdentities(); + public List getIdentities() { + return account.getIdentityKeyStore() + .getIdentities() + .stream() + .map(this::toIdentity) + .collect(Collectors.toList()); + } + + private Identity toIdentity(final IdentityInfo identityInfo) { + if (identityInfo == null) { + return null; + } + + final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); + return new Identity(address, + identityInfo.getIdentityKey(), + computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), + computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), + identityInfo.getTrustLevel(), + identityInfo.getDateAdded()); } @Override - public List getIdentities(RecipientIdentifier.Single recipient) { + public List getIdentities(RecipientIdentifier.Single recipient) { IdentityInfo identity; try { identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); } catch (UnregisteredUserException e) { identity = null; } - return identity == null ? List.of() : List.of(identity); + return identity == null ? List.of() : List.of(toIdentity(identity)); } /** @@ -1144,8 +1202,7 @@ public class ManagerImpl implements Manager { return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); } - @Override - public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + private byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); } @@ -1165,13 +1222,7 @@ public class ManagerImpl implements Manager { return resolveSignalServiceAddress(resolveRecipient(address)); } - @Override - public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); - } - - @Override - public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + private SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); if (address.getUuid().isPresent()) { return address.toSignalServiceAddress(); @@ -1180,13 +1231,15 @@ public class ManagerImpl implements Manager { // Address in recipient store doesn't have a uuid, this shouldn't happen // Try to retrieve the uuid from the server final var number = address.getNumber().get(); + final UUID uuid; try { - return resolveSignalServiceAddress(getRegisteredUser(number)); + uuid = getRegisteredUser(number); } catch (IOException e) { logger.warn("Failed to get uuid for e164 number: {}", number, e); // Return SignalServiceAddress with unknown UUID return address.toSignalServiceAddress(); } + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); } private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 978f1fd5..ff94c19b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -91,18 +91,18 @@ public class RegistrationManager implements Closeable { } public static RegistrationManager init( - String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent + String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent ) throws IOException { var pathConfig = PathConfig.createDefault(settingsPath); final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent); - if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { + if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) { var identityKey = KeyUtils.generateIdentityKeyPair(); var registrationId = KeyHelper.generateRegistrationId(false); var profileKey = KeyUtils.createProfileKey(); var account = SignalAccount.create(pathConfig.getDataPath(), - username, + number, identityKey, registrationId, profileKey, @@ -111,7 +111,7 @@ public class RegistrationManager implements Closeable { return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); } - var account = SignalAccount.load(pathConfig.getDataPath(), username, true, TrustNewIdentity.ON_FIRST_USE); + var account = SignalAccount.load(pathConfig.getDataPath(), number, true, TrustNewIdentity.ON_FIRST_USE); return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); } diff --git a/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java b/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java index d506f0c6..905392c5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java +++ b/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java @@ -4,16 +4,16 @@ import java.io.File; public class UserAlreadyExists extends Exception { - private final String username; + private final String number; private final File fileName; - public UserAlreadyExists(String username, File fileName) { - this.username = username; + public UserAlreadyExists(String number, File fileName) { + this.number = number; this.fileName = fileName; } - public String getUsername() { - return username; + public String getNumber() { + return number; } public File getFileName() { diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Device.java b/lib/src/main/java/org/asamk/signal/manager/api/Device.java index 76074cbf..9ee0d36a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Device.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Device.java @@ -6,12 +6,14 @@ public class Device { private final String name; private final long created; private final long lastSeen; + private final boolean thisDevice; - public Device(long id, String name, long created, long lastSeen) { + public Device(long id, String name, long created, long lastSeen, final boolean thisDevice) { this.id = id; this.name = name; this.created = created; this.lastSeen = lastSeen; + this.thisDevice = thisDevice; } public long getId() { @@ -29,4 +31,8 @@ public class Device { public long getLastSeen() { return lastSeen; } + + public boolean isThisDevice() { + return thisDevice; + } } 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 new file mode 100644 index 00000000..650e10b6 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/Group.java @@ -0,0 +1,99 @@ +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.storage.recipients.RecipientAddress; + +import java.util.Set; + +public class Group { + + private final GroupId groupId; + private final String title; + private final String description; + private final GroupInviteLinkUrl groupInviteLinkUrl; + private final Set members; + private final Set pendingMembers; + private final Set requestingMembers; + private final Set adminMembers; + private final boolean isBlocked; + private final int messageExpirationTime; + private final boolean isAnnouncementGroup; + private final boolean isMember; + + public Group( + final GroupId groupId, + final String title, + final String description, + final GroupInviteLinkUrl groupInviteLinkUrl, + final Set members, + final Set pendingMembers, + final Set requestingMembers, + final Set adminMembers, + final boolean isBlocked, + final int messageExpirationTime, + final boolean isAnnouncementGroup, + final boolean isMember + ) { + this.groupId = groupId; + this.title = title; + this.description = description; + this.groupInviteLinkUrl = groupInviteLinkUrl; + this.members = members; + this.pendingMembers = pendingMembers; + this.requestingMembers = requestingMembers; + this.adminMembers = adminMembers; + this.isBlocked = isBlocked; + this.messageExpirationTime = messageExpirationTime; + this.isAnnouncementGroup = isAnnouncementGroup; + this.isMember = isMember; + } + + public GroupId getGroupId() { + return groupId; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public GroupInviteLinkUrl getGroupInviteLinkUrl() { + return groupInviteLinkUrl; + } + + public Set getMembers() { + return members; + } + + public Set getPendingMembers() { + return pendingMembers; + } + + public Set getRequestingMembers() { + return requestingMembers; + } + + public Set getAdminMembers() { + return adminMembers; + } + + public boolean isBlocked() { + return isBlocked; + } + + public int getMessageExpirationTime() { + return messageExpirationTime; + } + + public boolean isAnnouncementGroup() { + return isAnnouncementGroup; + } + + public boolean isMember() { + return isMember; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Identity.java b/lib/src/main/java/org/asamk/signal/manager/api/Identity.java new file mode 100644 index 00000000..4f6f21f6 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/Identity.java @@ -0,0 +1,65 @@ +package org.asamk.signal.manager.api; + +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.whispersystems.libsignal.IdentityKey; + +import java.util.Date; + +public class Identity { + + private final RecipientAddress recipient; + private final IdentityKey identityKey; + private final String safetyNumber; + private final byte[] scannableSafetyNumber; + private final TrustLevel trustLevel; + private final Date dateAdded; + + public Identity( + final RecipientAddress recipient, + final IdentityKey identityKey, + final String safetyNumber, + final byte[] scannableSafetyNumber, + final TrustLevel trustLevel, + final Date dateAdded + ) { + this.recipient = recipient; + this.identityKey = identityKey; + this.safetyNumber = safetyNumber; + this.scannableSafetyNumber = scannableSafetyNumber; + this.trustLevel = trustLevel; + this.dateAdded = dateAdded; + } + + public RecipientAddress getRecipient() { + return recipient; + } + + public IdentityKey getIdentityKey() { + return this.identityKey; + } + + public TrustLevel getTrustLevel() { + return this.trustLevel; + } + + boolean isTrusted() { + return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; + } + + public Date getDateAdded() { + return this.dateAdded; + } + + public byte[] getFingerprint() { + return identityKey.getPublicKey().serialize(); + } + + public String getSafetyNumber() { + return safetyNumber; + } + + public byte[] getScannableSafetyNumber() { + return scannableSafetyNumber; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index cb0a08bb..be1029e6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager.api; import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -29,6 +30,17 @@ public abstract class RecipientIdentifier { public static Single fromAddress(SignalServiceAddress address) { return new Uuid(address.getUuid()); } + + public static Single fromAddress(RecipientAddress address) { + if (address.getNumber().isPresent()) { + return new Number(address.getNumber().get()); + } else if (address.getUuid().isPresent()) { + return new Uuid(address.getUuid().get()); + } + throw new AssertionError("RecipientAddress without identifier"); + } + + public abstract String getIdentifier(); } public static class Uuid extends Single { @@ -53,6 +65,11 @@ public abstract class RecipientIdentifier { public int hashCode() { return uuid.hashCode(); } + + @Override + public String getIdentifier() { + return uuid.toString(); + } } public static class Number extends Single { @@ -77,6 +94,11 @@ public abstract class RecipientIdentifier { public int hashCode() { return number.hashCode(); } + + @Override + public String getIdentifier() { + return number; + } } public static class Group extends RecipientIdentifier { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java index 88877d83..c0f5b0b8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -57,6 +57,16 @@ public class RecipientAddress { } } + public String getLegacyIdentifier() { + if (e164.isPresent()) { + return e164.get(); + } else if (uuid.isPresent()) { + return uuid.get().toString(); + } else { + throw new AssertionError("Given the checks in the constructor, this should not be possible."); + } + } + public boolean matches(RecipientAddress other) { return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) || ( e164.isPresent() && other.e164.isPresent() && e164.get().equals(other.e164.get()) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index b19fba8d..2105ca76 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -13,6 +13,8 @@ import java.util.List; */ public interface Signal extends DBusInterface { + String getNumber(); + long sendMessage( String message, List attachments, String recipient ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity; @@ -26,7 +28,7 @@ public interface Signal extends DBusInterface { ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity; void sendReadReceipt( - String recipient, List targetSentTimestamp + String recipient, List messageIds ) throws Error.Failure, Error.UntrustedIdentity; long sendRemoteDeleteMessage( diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 4aa510d6..e81b7018 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -161,7 +161,7 @@ public class App { } if (username == null) { - var usernames = Manager.getAllLocalUsernames(dataPath); + var usernames = Manager.getAllLocalNumbers(dataPath); if (command instanceof MultiLocalCommand) { handleMultiLocalCommand((MultiLocalCommand) command, diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index bc9244f8..35790678 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -61,13 +61,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final var recipientName = getLegacyIdentifier(m.resolveSignalServiceAddress(e.getSender())); writer.println( "Use 'signal-cli -u {} listIdentities -n {}', verify the key and run 'signal-cli -u {} trust -v \"FINGER_PRINT\" {}' to mark it as trusted", - m.getUsername(), + m.getSelfNumber(), recipientName, - m.getUsername(), + m.getSelfNumber(), recipientName); writer.println( "If you don't care about security, use 'signal-cli -u {} trust -a {}' to trust it without verification", - m.getUsername(), + m.getSelfNumber(), recipientName); } else { writer.println("Exception: {} ({})", exception.getMessage(), exception.getClass().getSimpleName()); @@ -657,7 +657,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { private void printMention( PlainTextWriter writer, SignalServiceDataMessage.Mention mention ) { - final var address = m.resolveSignalServiceAddress(mention.getUuid()); + final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid())); writer.println("- {}: {} (length: {})", formatContact(address), mention.getStart(), mention.getLength()); } diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 5394022e..516224f5 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -37,7 +37,7 @@ public class BlockCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { final var contacts = ns.getList("recipient"); - for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getUsername())) { + for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getSelfNumber())) { try { m.setContactBlocked(contact, true); } catch (NotMasterDeviceException e) { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 4a322b99..9878de15 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -95,7 +95,7 @@ public class DaemonCommand implements MultiLocalCommand { try (var conn = DBusConnection.getConnection(busType)) { final var signalControl = new DbusSignalControlImpl(c, m -> { try { - final var objectPath = DbusConfig.getObjectPath(m.getUsername()); + final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber()); return run(conn, objectPath, m, outputWriter, ignoreAttachments); } catch (DBusException e) { logger.error("Failed to export object", e); diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index f5585881..1e06ea9c 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -57,14 +57,14 @@ public class JoinGroupCommand implements JsonRpcLocalCommand { var newGroupId = results.first(); if (outputWriter instanceof JsonWriter) { final var writer = (JsonWriter) outputWriter; - if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) { + if (!m.getGroup(newGroupId).isMember()) { writer.write(Map.of("groupId", newGroupId.toBase64(), "onlyRequested", true)); } else { writer.write(Map.of("groupId", newGroupId.toBase64())); } } else { final var writer = (PlainTextWriter) outputWriter; - if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) { + if (!m.getGroup(newGroupId).isMember()) { writer.println("Requested to join group \"{}\"", newGroupId.toBase64()); } else { writer.println("Joined group \"{}\"", newGroupId.toBase64()); diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java index fbc03300..1d697299 100644 --- a/src/main/java/org/asamk/signal/commands/LinkCommand.java +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -44,7 +44,7 @@ public class LinkCommand implements ProvisioningCommand { try { writer.println("{}", m.getDeviceLinkUri()); try (var manager = m.finishDeviceLink(deviceName)) { - writer.println("Associated with: {}", manager.getUsername()); + writer.println("Associated with: {}", manager.getSelfNumber()); } } catch (TimeoutException e) { throw new UserErrorException("Link request timed out, please try again."); @@ -52,7 +52,7 @@ public class LinkCommand implements ProvisioningCommand { throw new IOErrorException("Link request error: " + e.getMessage(), e); } catch (UserAlreadyExists e) { throw new UserErrorException("The user " - + e.getUsername() + + e.getNumber() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again."); diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 5e609a48..b6dfc3ce 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -8,10 +8,9 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.manager.Manager; +import java.util.UUID; import java.util.stream.Collectors; -import static org.asamk.signal.util.Util.getLegacyIdentifier; - public class ListContactsCommand implements JsonRpcLocalCommand { @Override @@ -33,7 +32,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand { for (var c : contacts) { final var contact = c.second(); writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}", - getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())), + c.first().getLegacyIdentifier(), contact.getName(), contact.isBlocked(), contact.getMessageExpirationTime() == 0 @@ -43,10 +42,10 @@ public class ListContactsCommand implements JsonRpcLocalCommand { } else { final var writer = (JsonWriter) outputWriter; final var jsonContacts = contacts.stream().map(contactPair -> { - final var address = m.resolveSignalServiceAddress(contactPair.first()); + final var address = contactPair.first(); final var contact = contactPair.second(); - return new JsonContact(address.getNumber().orNull(), - address.getUuid().toString(), + return new JsonContact(address.getNumber().orElse(null), + address.getUuid().map(UUID::toString).orElse(null), contact.getName(), contact.isBlocked(), contact.getMessageExpirationTime()); diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index ad0d3531..1de5b842 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -46,7 +46,7 @@ public class ListDevicesCommand implements JsonRpcLocalCommand { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; for (var d : devices) { - writer.println("- Device {}{}:", d.getId(), (d.getId() == m.getDeviceId() ? " (this device)" : "")); + writer.println("- Device {}{}:", d.getId(), (d.isThisDevice() ? " (this device)" : "")); writer.indent(w -> { w.println("Name: {}", d.getName()); w.println("Created: {}", DateUtils.formatTimestamp(d.getCreated())); diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index b53577be..1eda53ce 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -9,13 +9,13 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.storage.groups.GroupInfo; -import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; public class ListGroupsCommand implements JsonRpcLocalCommand { @@ -35,44 +35,41 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { .help("List the members and group invite links of each group. If output=json, then this is always set"); } - private static Set resolveMembers(Manager m, Set addresses) { - return addresses.stream() - .map(m::resolveSignalServiceAddress) - .map(Util::getLegacyIdentifier) - .collect(Collectors.toSet()); + private static Set resolveMembers(Set addresses) { + return addresses.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toSet()); } - private static Set resolveJsonMembers(Manager m, Set addresses) { + private static Set resolveJsonMembers(Set addresses) { return addresses.stream() - .map(m::resolveSignalServiceAddress) - .map(address -> new JsonGroupMember(address.getNumber().orNull(), address.getUuid().toString())) + .map(address -> new JsonGroupMember(address.getNumber().orElse(null), + address.getUuid().map(UUID::toString).orElse(null))) .collect(Collectors.toSet()); } private static void printGroupPlainText( - PlainTextWriter writer, Manager m, GroupInfo group, boolean detailed + PlainTextWriter writer, Group group, boolean detailed ) { if (detailed) { - final var groupInviteLink = group.getGroupInviteLink(); + final var groupInviteLink = group.getGroupInviteLinkUrl(); writer.println( "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Message expiration: {} Link: {}", group.getGroupId().toBase64(), group.getTitle(), group.getDescription(), - group.isMember(m.getSelfRecipientId()), + group.isMember(), group.isBlocked(), - resolveMembers(m, group.getMembers()), - resolveMembers(m, group.getPendingMembers()), - resolveMembers(m, group.getRequestingMembers()), - resolveMembers(m, group.getAdminMembers()), + resolveMembers(group.getMembers()), + resolveMembers(group.getPendingMembers()), + resolveMembers(group.getRequestingMembers()), + resolveMembers(group.getAdminMembers()), group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s", groupInviteLink == null ? '-' : groupInviteLink.getUrl()); } else { writer.println("Id: {} Name: {} Active: {} Blocked: {}", group.getGroupId().toBase64(), group.getTitle(), - group.isMember(m.getSelfRecipientId()), + group.isMember(), group.isBlocked()); } } @@ -87,18 +84,18 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { final var jsonWriter = (JsonWriter) outputWriter; var jsonGroups = groups.stream().map(group -> { - final var groupInviteLink = group.getGroupInviteLink(); + final var groupInviteLink = group.getGroupInviteLinkUrl(); return new JsonGroup(group.getGroupId().toBase64(), group.getTitle(), group.getDescription(), - group.isMember(m.getSelfRecipientId()), + group.isMember(), group.isBlocked(), group.getMessageExpirationTime(), - resolveJsonMembers(m, group.getMembers()), - resolveJsonMembers(m, group.getPendingMembers()), - resolveJsonMembers(m, group.getRequestingMembers()), - resolveJsonMembers(m, group.getAdminMembers()), + resolveJsonMembers(group.getMembers()), + resolveJsonMembers(group.getPendingMembers()), + resolveJsonMembers(group.getRequestingMembers()), + resolveJsonMembers(group.getAdminMembers()), groupInviteLink == null ? null : groupInviteLink.getUrl()); }).collect(Collectors.toList()); @@ -107,7 +104,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { final var writer = (PlainTextWriter) outputWriter; boolean detailed = ns.getBoolean("detailed"); for (var group : groups) { - printGroupPlainText(writer, m, group, detailed); + printGroupPlainText(writer, group, detailed); } } } diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index 02cd1d9f..ed2942a5 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -8,7 +8,7 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.api.Identity; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; @@ -29,9 +29,9 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { return "listIdentities"; } - private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) { - final SignalServiceAddress address = m.resolveSignalServiceAddress(theirId.getRecipientId()); - var digits = Util.formatSafetyNumber(m.computeSafetyNumber(address, theirId.getIdentityKey())); + private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, Identity theirId) { + final SignalServiceAddress address = theirId.getRecipient().toSignalServiceAddress(); + var digits = Util.formatSafetyNumber(theirId.getSafetyNumber()); writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}", address.getNumber().orNull(), theirId.getTrustLevel(), @@ -52,11 +52,11 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { ) throws CommandException { var number = ns.getString("number"); - List identities; + List identities; if (number == null) { identities = m.getIdentities(); } else { - identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getUsername())); + identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getSelfNumber())); } if (outputWriter instanceof PlainTextWriter) { @@ -67,9 +67,9 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { } else { final var writer = (JsonWriter) outputWriter; final var jsonIdentities = identities.stream().map(id -> { - final var address = m.resolveSignalServiceAddress(id.getRecipientId()); - var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(address, id.getIdentityKey())); - var scannableSafetyNumber = m.computeSafetyNumberForScanning(address, id.getIdentityKey()); + final var address = id.getRecipient().toSignalServiceAddress(); + var safetyNumber = Util.formatSafetyNumber(id.getSafetyNumber()); + var scannableSafetyNumber = id.getScannableSafetyNumber(); return new JsonIdentity(address.getNumber().orNull(), address.getUuid().toString(), Hex.toString(id.getFingerprint()), diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 67a6596b..7635f8ae 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -50,7 +50,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { ) throws CommandException { final var groupId = CommandUtil.getGroupId(ns.getString("group-id")); - var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getUsername()); + var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getSelfNumber()); try { try { diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 338e70ac..f8c3c358 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -72,7 +72,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { try { final var results = m.sendMessageReaction(emoji, isRemove, - CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetTimestamp, recipientIdentifiers); outputResult(outputWriter, results.getTimestamp()); diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java index 0d5772ec..5dd29682 100644 --- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -37,7 +37,7 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { final var recipientString = ns.getString("recipient"); - final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); + final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber()); final var targetTimestamps = ns.getList("target-timestamp"); final var type = ns.getString("type"); diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index 3a965e47..cfe66770 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -45,7 +45,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand { final var recipientIdentifiers = new HashSet(); if (recipientStrings != null) { - final var localNumber = m.getUsername(); + final var localNumber = m.getSelfNumber(); recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber)); } if (groupIdStrings != null) { diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index aedc2c3e..9e59ad86 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -38,7 +38,7 @@ public class TrustCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { var recipentString = ns.getString("recipient"); - var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getUsername()); + var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getSelfNumber()); if (ns.getBoolean("trust-all-known-keys")) { boolean res = m.trustIdentityAllKeys(recipient); if (!res) { diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 812065bc..7cf209fa 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -36,7 +36,8 @@ public class UnblockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"), m.getUsername())) { + for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"), + m.getSelfNumber())) { try { m.setContactBlocked(contactNumber, false); } catch (NotMasterDeviceException e) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 6c2916eb..46641668 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -33,7 +33,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { var recipientString = ns.getString("recipient"); - var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); + var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber()); try { var expiration = ns.getInt("expiration"); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index b0269894..49cd4719 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -116,7 +116,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { final var groupIdString = ns.getString("group-id"); var groupId = CommandUtil.getGroupId(groupIdString); - final var localNumber = m.getUsername(); + final var localNumber = m.getSelfNumber(); var groupName = ns.getString("name"); var groupDescription = ns.getString("description"); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index 6ec8d964..be628bde 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -160,7 +160,7 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { synchronized (receiveThreads) { return receiveThreads.stream() .map(Pair::first) - .map(Manager::getUsername) + .map(Manager::getSelfNumber) .map(u -> new DBusPath(DbusConfig.getObjectPath(u))) .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 e975a671..c8208774 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.Device; +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; @@ -17,9 +18,9 @@ import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.util.ErrorUtils; -import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; @@ -45,8 +46,6 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.asamk.signal.util.Util.getLegacyIdentifier; - public class DbusSignalImpl implements Signal { private final Manager m; @@ -67,6 +66,11 @@ public class DbusSignalImpl implements Signal { return objectPath; } + @Override + public String getNumber() { + return m.getSelfNumber(); + } + @Override public void addDevice(String uri) { try { @@ -123,7 +127,7 @@ public class DbusSignalImpl implements Signal { public long sendMessage(final String message, final List attachments, final List recipients) { try { final var results = m.sendMessage(new Message(message, attachments), - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); @@ -153,7 +157,7 @@ public class DbusSignalImpl implements Signal { ) { try { final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); checkSendMessageResults(results.getTimestamp(), results.getResults()); @@ -205,9 +209,9 @@ public class DbusSignalImpl implements Signal { try { final var results = m.sendMessageReaction(emoji, remove, - getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetSentTimestamp, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); checkSendMessageResults(results.getTimestamp(), results.getResults()); @@ -227,7 +231,7 @@ public class DbusSignalImpl implements Signal { var recipients = new ArrayList(1); recipients.add(recipient); m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); } catch (IOException e) { @@ -241,10 +245,10 @@ public class DbusSignalImpl implements Signal { @Override public void sendReadReceipt( - final String recipient, final List timestamps + final String recipient, final List messageIds ) throws Error.Failure, Error.UntrustedIdentity { try { - m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getUsername()), timestamps); + m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()), messageIds); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (UntrustedIdentityException e) { @@ -291,7 +295,7 @@ public class DbusSignalImpl implements Signal { @Override public void sendEndSessionMessage(final List recipients) { try { - final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername())); + final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); checkSendMessageResults(results.getTimestamp(), results.getResults()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); @@ -325,7 +329,7 @@ public class DbusSignalImpl implements Signal { try { final var results = m.sendMessageReaction(emoji, remove, - getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetSentTimestamp, Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); checkSendMessageResults(results.getTimestamp(), results.getResults()); @@ -341,13 +345,13 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername())); + return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); } @Override public void setContactName(final String number, final String name) { try { - m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name); + m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } catch (UnregisteredUserException e) { @@ -358,7 +362,7 @@ public class DbusSignalImpl implements Signal { @Override public void setExpirationTimer(final String number, final int expiration) { try { - m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getUsername()), expiration); + m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getSelfNumber()), expiration); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } @@ -367,7 +371,7 @@ public class DbusSignalImpl implements Signal { @Override public void setContactBlocked(final String number, final boolean blocked) { try { - m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked); + m.setContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()), blocked); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } catch (IOException e) { @@ -412,11 +416,7 @@ public class DbusSignalImpl implements Signal { if (group == null) { return List.of(); } else { - return group.getMembers() - .stream() - .map(m::resolveSignalServiceAddress) - .map(Util::getLegacyIdentifier) - .collect(Collectors.toList()); + return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); } } @@ -432,7 +432,7 @@ public class DbusSignalImpl implements Signal { if (avatar.isEmpty()) { avatar = null; } - final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername()); + final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber()); if (groupId == null) { final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar)); checkSendMessageResults(results.second().getTimestamp(), results.second().getResults()); @@ -573,10 +573,9 @@ public class DbusSignalImpl implements Signal { // all numbers the system knows @Override public List listNumbers() { - return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId), + return Stream.concat(m.getIdentities().stream().map(Identity::getRecipient), m.getContacts().stream().map(Pair::first)) - .map(m::resolveSignalServiceAddress) - .map(a -> a.getNumber().orNull()) + .map(a -> a.getNumber().orElse(null)) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); @@ -589,16 +588,19 @@ public class DbusSignalImpl implements Signal { var contacts = m.getContacts(); for (var c : contacts) { if (name.equals(c.second().getName())) { - numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first()))); + numbers.add(c.first().getLegacyIdentifier()); } } // Try profiles if no contact name was found for (var identity : m.getIdentities()) { - final var recipientId = identity.getRecipientId(); - final var address = m.resolveSignalServiceAddress(recipientId); - var number = address.getNumber().orNull(); + final var address = identity.getRecipient(); + var number = address.getNumber().orElse(null); if (number != null) { - var profile = m.getRecipientProfile(recipientId); + Profile profile = null; + try { + profile = m.getRecipientProfile(RecipientIdentifier.Single.fromAddress(address)); + } catch (UnregisteredUserException ignored) { + } if (profile != null && profile.getDisplayName().equals(name)) { numbers.add(number); } @@ -639,7 +641,7 @@ public class DbusSignalImpl implements Signal { @Override public boolean isContactBlocked(final String number) { - return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername())); + return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber())); } @Override @@ -658,7 +660,7 @@ public class DbusSignalImpl implements Signal { if (group == null) { return false; } else { - return group.isMember(m.getSelfRecipientId()); + return group.isMember(); } } diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java index b24768b7..3c6f2eec 100644 --- a/src/main/java/org/asamk/signal/json/JsonMention.java +++ b/src/main/java/org/asamk/signal/json/JsonMention.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -26,7 +27,7 @@ public class JsonMention { final int length; JsonMention(SignalServiceDataMessage.Mention mention, Manager m) { - final var address = m.resolveSignalServiceAddress(mention.getUuid()); + final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid())); this.name = getLegacyIdentifier(address); this.number = address.getNumber().orNull(); this.uuid = address.getUuid().toString(); diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 7b884b0e..e49e6125 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -86,7 +86,7 @@ public class JsonMessageEnvelope { } String name; try { - name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getUsername())); + name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getSelfNumber())); } catch (InvalidNumberException | NullPointerException e) { name = null; } diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java index 18b38a2a..0a624e6b 100644 --- a/src/main/java/org/asamk/signal/util/CommandUtil.java +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -28,7 +28,7 @@ public class CommandUtil { recipientIdentifiers.add(RecipientIdentifier.NoteToSelf.INSTANCE); } if (recipientStrings != null) { - final var localNumber = m.getUsername(); + final var localNumber = m.getSelfNumber(); recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber)); } if (groupIdStrings != null) { From 593cd7d8ca6e8e0ab654accfd7e3c9d2ee01b001 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 18:51:44 +0200 Subject: [PATCH 16/26] Refactor dbus client mode to improve maintainability --- src/main/java/org/asamk/Signal.java | 2 +- src/main/java/org/asamk/signal/App.java | 13 +- .../asamk/signal/commands/DbusCommand.java | 20 - .../signal/commands/RemoteDeleteCommand.java | 46 +- .../asamk/signal/commands/SendCommand.java | 97 +--- .../signal/commands/SendReactionCommand.java | 53 +- .../signal/commands/UpdateGroupCommand.java | 43 +- .../exceptions/UserErrorException.java | 4 + .../asamk/signal/dbus/DbusManagerImpl.java | 487 ++++++++++++++++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 57 +- 10 files changed, 531 insertions(+), 291 deletions(-) delete mode 100644 src/main/java/org/asamk/signal/commands/DbusCommand.java create mode 100644 src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 2105ca76..cc521f6d 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -13,7 +13,7 @@ import java.util.List; */ public interface Signal extends DBusInterface { - String getNumber(); + String getSelfNumber(); long sendMessage( String message, List attachments, String recipient diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index e81b7018..bffbded5 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -8,7 +8,6 @@ import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.Signal; import org.asamk.signal.commands.Command; import org.asamk.signal.commands.Commands; -import org.asamk.signal.commands.DbusCommand; import org.asamk.signal.commands.ExtendedDbusCommand; import org.asamk.signal.commands.LocalCommand; import org.asamk.signal.commands.MultiLocalCommand; @@ -19,6 +18,7 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.dbus.DbusManagerImpl; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotRegisteredException; import org.asamk.signal.manager.ProvisioningManager; @@ -29,6 +29,7 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.util.IOUtils; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -346,8 +347,14 @@ public class App { ) throws CommandException { if (command instanceof ExtendedDbusCommand) { ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn, outputWriter); - } else if (command instanceof DbusCommand) { - ((DbusCommand) command).handleCommand(ns, ts, outputWriter); + } else if (command instanceof LocalCommand) { + try { + ((LocalCommand) command).handleCommand(ns, new DbusManagerImpl(ts), outputWriter); + } catch (UnsupportedOperationException e) { + throw new UserErrorException("Command is not yet implemented via dbus", e); + } catch (DBusExecutionException e) { + throw new UnexpectedErrorException(e.getMessage(), e); + } } else { throw new UserErrorException("Command is not yet implemented via dbus"); } diff --git a/src/main/java/org/asamk/signal/commands/DbusCommand.java b/src/main/java/org/asamk/signal/commands/DbusCommand.java deleted file mode 100644 index 9f676a39..00000000 --- a/src/main/java/org/asamk/signal/commands/DbusCommand.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.asamk.signal.commands; - -import net.sourceforge.argparse4j.inf.Namespace; - -import org.asamk.Signal; -import org.asamk.signal.OutputWriter; -import org.asamk.signal.commands.exceptions.CommandException; -import org.asamk.signal.dbus.DbusSignalImpl; -import org.asamk.signal.manager.Manager; - -public interface DbusCommand extends LocalCommand { - - void handleCommand(Namespace ns, Signal signal, OutputWriter outputWriter) throws CommandException; - - default void handleCommand( - final Namespace ns, final Manager m, final OutputWriter outputWriter - ) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); - } -} diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index 7d7067c4..e515defe 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.Signal; import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; @@ -17,13 +16,11 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; -import org.freedesktop.dbus.errors.UnknownObject; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import java.io.IOException; import java.util.Map; -public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { +public class RemoteDeleteCommand implements JsonRpcLocalCommand { @Override public String getName() { @@ -69,47 +66,6 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Signal signal, final OutputWriter outputWriter - ) throws CommandException { - final var recipients = ns.getList("recipient"); - final var groupIdStrings = ns.getList("group-id"); - - final var noRecipients = recipients == null || recipients.isEmpty(); - final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); - if (noRecipients && noGroups) { - throw new UserErrorException("No recipients given"); - } - if (!noRecipients && !noGroups) { - throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); - } - - final long targetTimestamp = ns.getLong("target-timestamp"); - - try { - long timestamp = 0; - if (!noGroups) { - final var groupIds = CommandUtil.getGroupIds(groupIdStrings); - for (final var groupId : groupIds) { - timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId.serialize()); - } - } else { - timestamp = signal.sendRemoteDeleteMessage(targetTimestamp, recipients); - } - outputResult(outputWriter, timestamp); - } catch (UnknownObject e) { - throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); - } catch (Signal.Error.InvalidNumber e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); - } catch (Signal.Error.GroupNotFound e) { - throw new UserErrorException("Failed to send to group: " + e.getMessage()); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 1973b1a1..1cd2e674 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -4,13 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.Signal; import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; -import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; @@ -22,8 +20,6 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.IOUtils; -import org.freedesktop.dbus.errors.UnknownObject; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +29,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -public class SendCommand implements DbusCommand, JsonRpcLocalCommand { +public class SendCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(SendCommand.class); @@ -116,97 +112,6 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Signal signal, final OutputWriter outputWriter - ) throws CommandException { - final var recipients = ns.getList("recipient"); - final var isEndSession = ns.getBoolean("end-session"); - final var groupIdStrings = ns.getList("group-id"); - final var isNoteToSelf = ns.getBoolean("note-to-self"); - - final var noRecipients = recipients == null || recipients.isEmpty(); - final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); - if ((noRecipients && isEndSession) || (noRecipients && noGroups && !isNoteToSelf)) { - throw new UserErrorException("No recipients given"); - } - if (!noRecipients && !noGroups) { - throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); - } - if (!noRecipients && isNoteToSelf) { - throw new UserErrorException( - "You cannot specify recipients by phone number and note to self at the same time"); - } - - if (isEndSession) { - try { - signal.sendEndSessionMessage(recipients); - return; - } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - - var messageText = ns.getString("message"); - if (messageText == null) { - try { - messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); - } catch (IOException e) { - throw new UserErrorException("Failed to read message from stdin: " + e.getMessage()); - } - } - - List attachments = ns.getList("attachment"); - if (attachments == null) { - attachments = List.of(); - } - - if (!noGroups) { - final var groupIds = CommandUtil.getGroupIds(groupIdStrings); - - try { - long timestamp = 0; - for (final var groupId : groupIds) { - timestamp = signal.sendGroupMessage(messageText, attachments, groupId.serialize()); - } - outputResult(outputWriter, timestamp); - return; - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage(), e); - } - } - - if (isNoteToSelf) { - try { - var timestamp = signal.sendNoteToSelfMessage(messageText, attachments); - outputResult(outputWriter, timestamp); - return; - } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage(), e); - } - } - - try { - var timestamp = signal.sendMessage(messageText, attachments, recipients); - outputResult(outputWriter, timestamp); - } catch (UnknownObject e) { - throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); - } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index f8c3c358..a1c6c319 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.Signal; import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; @@ -17,13 +16,11 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; -import org.freedesktop.dbus.errors.UnknownObject; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import java.io.IOException; import java.util.Map; -public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { +public class SendReactionCommand implements JsonRpcLocalCommand { @Override public String getName() { @@ -85,54 +82,6 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Signal signal, final OutputWriter outputWriter - ) throws CommandException { - final var recipients = ns.getList("recipient"); - final var groupIdStrings = ns.getList("group-id"); - - final var noRecipients = recipients == null || recipients.isEmpty(); - final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); - if (noRecipients && noGroups) { - throw new UserErrorException("No recipients given"); - } - if (!noRecipients && !noGroups) { - throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); - } - - final var emoji = ns.getString("emoji"); - final var isRemove = ns.getBoolean("remove"); - final var targetAuthor = ns.getString("target-author"); - final var targetTimestamp = ns.getLong("target-timestamp"); - - try { - long timestamp = 0; - if (!noGroups) { - final var groupIds = CommandUtil.getGroupIds(groupIdStrings); - for (final var groupId : groupIds) { - timestamp = signal.sendGroupMessageReaction(emoji, - isRemove, - targetAuthor, - targetTimestamp, - groupId.serialize()); - } - } else { - timestamp = signal.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, recipients); - } - outputResult(outputWriter, timestamp); - } catch (UnknownObject e) { - throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); - } catch (Signal.Error.InvalidNumber e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); - } catch (Signal.Error.GroupNotFound e) { - throw new UserErrorException("Failed to send to group: " + e.getMessage()); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 49cd4719..4bbaa992 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.Signal; import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; @@ -21,17 +20,14 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; -public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { +public class UpdateGroupCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(UpdateGroupCommand.class); @@ -179,43 +175,6 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Signal signal, final OutputWriter outputWriter - ) throws CommandException { - var groupId = CommandUtil.getGroupId(ns.getString("group-id")); - - var groupName = ns.getString("name"); - if (groupName == null) { - groupName = ""; - } - - List groupMembers = ns.getList("member"); - if (groupMembers == null) { - groupMembers = new ArrayList<>(); - } - - var groupAvatar = ns.getString("avatar"); - if (groupAvatar == null) { - groupAvatar = ""; - } - - try { - var newGroupId = signal.updateGroup(groupId == null ? new byte[0] : groupId.serialize(), - groupName, - groupMembers, - groupAvatar); - if (groupId == null) { - outputResult(outputWriter, null, GroupId.unknownVersion(newGroupId)); - } - } catch (Signal.Error.AttachmentInvalid e) { - throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - private void outputResult(final OutputWriter outputWriter, final Long timestamp, final GroupId groupId) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java b/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java index 84e957cc..819ce495 100644 --- a/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java +++ b/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java @@ -5,4 +5,8 @@ public final class UserErrorException extends CommandException { public UserErrorException(final String message) { super(message); } + + public UserErrorException(final String message, final Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java new file mode 100644 index 00000000..b9f5ae11 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -0,0 +1,487 @@ +package org.asamk.signal.dbus; + +import org.asamk.Signal; +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotMasterDeviceException; +import org.asamk.signal.manager.StickerPackInvalidException; +import org.asamk.signal.manager.UntrustedIdentityException; +import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.Identity; +import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; +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.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; +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; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * This class implements the Manager interface using the DBus Signal interface, where possible. + * It's used for the signal-cli dbus client mode (--dbus, --dbus-system) + */ +public class DbusManagerImpl implements Manager { + + private final Signal signal; + + public DbusManagerImpl(final Signal signal) { + this.signal = signal; + } + + @Override + public String getSelfNumber() { + return signal.getSelfNumber(); + } + + @Override + public void checkAccountState() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Map> areUsersRegistered(final Set numbers) throws IOException { + final var numbersList = new ArrayList<>(numbers); + final var registered = signal.isRegistered(numbersList); + + final var result = new HashMap>(); + for (var i = 0; i < numbersList.size(); i++) { + result.put(numbersList.get(i), + new Pair<>(numbersList.get(i), registered.get(i) ? UuidUtil.UNKNOWN_UUID : null)); + } + return result; + } + + @Override + public void updateAccountAttributes(final String deviceName) throws IOException { + if (deviceName != null) { + signal.updateDeviceName(deviceName); + } + } + + @Override + public void setProfile( + final String givenName, + final String familyName, + final String about, + final String aboutEmoji, + final Optional avatar + ) throws IOException { + signal.updateProfile(emptyIfNull(givenName), + emptyIfNull(familyName), + emptyIfNull(about), + emptyIfNull(aboutEmoji), + avatar == null ? "" : avatar.transform(File::getPath).or(""), + avatar != null && !avatar.isPresent()); + } + + @Override + public void unregister() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void deleteAccount() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public List getLinkedDevices() throws IOException { + return signal.listDevices() + .stream() + .map(name -> new Device(-1, name, 0, 0, false)) + .collect(Collectors.toList()); + } + + @Override + public void removeLinkedDevices(final int deviceId) throws IOException { + signal.removeDevice(deviceId); + } + + @Override + public void addDeviceLink(final URI linkUri) throws IOException, InvalidKeyException { + signal.addDevice(linkUri.toString()); + } + + @Override + public void setRegistrationLockPin(final Optional pin) throws IOException, UnauthenticatedResponseException { + if (pin.isPresent()) { + signal.setPin(pin.get()); + } else { + signal.removePin(); + } + } + + @Override + public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { + throw new UnsupportedOperationException(); + } + + @Override + public List getGroups() { + final var groupIds = signal.getGroupIds(); + return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList()); + } + + @Override + public SendGroupMessageResults quitGroup( + final GroupId groupId, final Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { + if (groupAdmins.size() > 0) { + throw new UnsupportedOperationException(); + } + signal.quitGroup(groupId.serialize()); + return new SendGroupMessageResults(0, List.of()); + } + + @Override + public void deleteGroup(final GroupId groupId) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + 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), + members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), + avatarFile == null ? "" : avatarFile.getPath()); + return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); + } + + @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 + ) 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()); + return new SendGroupMessageResults(0, List.of()); + } + + @Override + public Pair joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException { + final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl()); + return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); + } + + @Override + public void sendTypingMessage( + final TypingAction action, final Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(), + action == TypingAction.STOP); + } else if (recipient instanceof RecipientIdentifier.Group) { + throw new UnsupportedOperationException(); + } + } + } + + @Override + public void sendReadReceipt( + final RecipientIdentifier.Single sender, final List messageIds + ) throws IOException, UntrustedIdentityException { + signal.sendReadReceipt(sender.getIdentifier(), messageIds); + } + + @Override + public void sendViewedReceipt( + final RecipientIdentifier.Single sender, final List messageIds + ) throws IOException, UntrustedIdentityException { + throw new UnsupportedOperationException(); + } + + @Override + public SendMessageResults sendMessage( + final Message message, final Set recipients + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return handleMessage(recipients, + numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers), + () -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()), + groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId)); + } + + @Override + public SendMessageResults sendRemoteDeleteMessage( + final long targetSentTimestamp, final Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return handleMessage(recipients, + numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers), + () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()), + groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId)); + } + + @Override + public SendMessageResults sendMessageReaction( + final String emoji, + final boolean remove, + final RecipientIdentifier.Single targetAuthor, + final long targetSentTimestamp, + final Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return handleMessage(recipients, + numbers -> signal.sendMessageReaction(emoji, + remove, + targetAuthor.getIdentifier(), + targetSentTimestamp, + numbers), + () -> signal.sendMessageReaction(emoji, + remove, + targetAuthor.getIdentifier(), + targetSentTimestamp, + signal.getSelfNumber()), + groupId -> signal.sendGroupMessageReaction(emoji, + remove, + targetAuthor.getIdentifier(), + targetSentTimestamp, + groupId)); + } + + @Override + public SendMessageResults sendEndSessionMessage(final Set recipients) throws IOException { + signal.sendEndSessionMessage(recipients.stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + return new SendMessageResults(0, Map.of()); + } + + @Override + public void setContactName( + final RecipientIdentifier.Single recipient, final String name + ) throws NotMasterDeviceException, UnregisteredUserException { + signal.setContactName(recipient.getIdentifier(), name); + } + + @Override + public void setContactBlocked( + final RecipientIdentifier.Single recipient, final boolean blocked + ) throws NotMasterDeviceException, IOException { + signal.setContactBlocked(recipient.getIdentifier(), blocked); + } + + @Override + public void setGroupBlocked( + final GroupId groupId, final boolean blocked + ) throws GroupNotFoundException, IOException { + signal.setGroupBlocked(groupId.serialize(), blocked); + } + + @Override + public void setExpirationTimer( + final RecipientIdentifier.Single recipient, final int messageExpirationTimer + ) throws IOException { + signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer); + } + + @Override + public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException { + try { + return new URI(signal.uploadStickerPack(path.getPath())); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + @Override + public void requestAllSyncData() throws IOException { + signal.sendSyncRequest(); + } + + @Override + public void receiveMessages( + final long timeout, + final TimeUnit unit, + final boolean returnOnTimeout, + final boolean ignoreAttachments, + final ReceiveMessageHandler handler + ) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasCaughtUpWithOldMessages() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { + return signal.isContactBlocked(recipient.getIdentifier()); + } + + @Override + public File getAttachmentFile(final SignalServiceAttachmentRemoteId attachmentId) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendContacts() throws IOException { + signal.sendContacts(); + } + + @Override + public List> getContacts() { + throw new UnsupportedOperationException(); + } + + @Override + public String getContactOrProfileName(final RecipientIdentifier.Single recipient) { + return signal.getContactName(recipient.getIdentifier()); + } + + @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)); + } + + @Override + public List getIdentities() { + throw new UnsupportedOperationException(); + } + + @Override + public List getIdentities(final RecipientIdentifier.Single recipient) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean trustIdentityVerifiedSafetyNumber( + final RecipientIdentifier.Single recipient, final String safetyNumber + ) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean trustIdentityVerifiedSafetyNumber( + final RecipientIdentifier.Single recipient, final byte[] safetyNumber + ) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) { + 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; + } + + @Override + public void close() throws IOException { + } + + private SendMessageResults handleMessage( + Set recipients, + Function, Long> recipientsHandler, + Supplier noteToSelfHandler, + Function groupHandler + ) { + long timestamp = 0; + final var singleRecipients = recipients.stream() + .filter(r -> r instanceof RecipientIdentifier.Single) + .map(RecipientIdentifier.Single.class::cast) + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList()); + if (singleRecipients.size() > 0) { + timestamp = recipientsHandler.apply(singleRecipients); + } + + if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) { + timestamp = noteToSelfHandler.get(); + } + final var groupRecipients = recipients.stream() + .filter(r -> r instanceof RecipientIdentifier.Group) + .map(RecipientIdentifier.Group.class::cast) + .map(g -> g.groupId) + .collect(Collectors.toList()); + for (final var groupId : groupRecipients) { + timestamp = groupHandler.apply(groupId.serialize()); + } + return new SendMessageResults(timestamp, Map.of()); + } + + private String emptyIfNull(final String string) { + return string == null ? "" : string; + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index c8208774..1a4fdc10 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -67,7 +67,7 @@ public class DbusSignalImpl implements Signal { } @Override - public String getNumber() { + public String getSelfNumber() { return m.getSelfNumber(); } @@ -96,8 +96,6 @@ public class DbusSignalImpl implements Signal { @Override public List listDevices() { List devices; - List results = new ArrayList(); - try { devices = m.getLinkedDevices(); } catch (IOException | Error.Failure e) { @@ -345,7 +343,8 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); + final var name = m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); + return name == null ? "" : name; } @Override @@ -403,7 +402,7 @@ public class DbusSignalImpl implements Signal { @Override public String getGroupName(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); - if (group == null) { + if (group == null || group.getTitle() == null) { return ""; } else { return group.getTitle(); @@ -423,15 +422,9 @@ public class DbusSignalImpl implements Signal { @Override public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) { try { - if (groupId.length == 0) { - groupId = null; - } - if (name.isEmpty()) { - name = null; - } - if (avatar.isEmpty()) { - avatar = null; - } + groupId = nullIfEmpty(groupId); + name = nullIfEmpty(name); + avatar = nullIfEmpty(avatar); final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber()); if (groupId == null) { final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar)); @@ -499,17 +492,19 @@ public class DbusSignalImpl implements Signal { @Override public void updateProfile( - final String givenName, - final String familyName, - final String about, - final String aboutEmoji, + String givenName, + String familyName, + String about, + String aboutEmoji, String avatarPath, final boolean removeAvatar ) { try { - if (avatarPath.isEmpty()) { - avatarPath = null; - } + givenName = nullIfEmpty(givenName); + familyName = nullIfEmpty(familyName); + about = nullIfEmpty(about); + aboutEmoji = nullIfEmpty(aboutEmoji); + avatarPath = nullIfEmpty(avatarPath); Optional avatarFile = removeAvatar ? Optional.absent() : avatarPath == null ? null : Optional.of(new File(avatarPath)); @@ -527,17 +522,7 @@ public class DbusSignalImpl implements Signal { String avatarPath, final boolean removeAvatar ) { - try { - if (avatarPath.isEmpty()) { - avatarPath = null; - } - Optional avatarFile = removeAvatar - ? Optional.absent() - : avatarPath == null ? null : Optional.of(new File(avatarPath)); - m.setProfile(name, null, about, aboutEmoji, avatarFile); - } catch (IOException e) { - throw new Error.Failure(e.getMessage()); - } + updateProfile(name, "", about, aboutEmoji, avatarPath, removeAvatar); } @Override @@ -766,4 +751,12 @@ public class DbusSignalImpl implements Signal { throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage()); } } + + private byte[] nullIfEmpty(final byte[] array) { + return array.length == 0 ? null : array; + } + + private String nullIfEmpty(final String name) { + return name.isEmpty() ? null : name; + } } From f44b148946df5822f11a755ce3a4fba2d91d3b68 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 23:48:16 +0200 Subject: [PATCH 17/26] Allow message from pending member if it's just a group update Fixes #751 --- .../org/asamk/signal/manager/helper/IncomingMessageHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 45173da4..64e16857 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -416,7 +416,7 @@ public final class IncomingMessageHandler { } final var recipientId = recipientResolver.resolveRecipient(source); - if (!group.isMember(recipientId)) { + if (!group.isMember(recipientId) && !(group.isPendingMember(recipientId) && message.isGroupV2Update())) { return true; } From c9f5550d1821ee99879c75db124baf46642fd846 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 30 Sep 2021 19:33:57 +0200 Subject: [PATCH 18/26] Remove workaround for getBoolean from JsonRpcLocalCommand --- src/main/java/org/asamk/signal/App.java | 4 ++-- src/main/java/org/asamk/signal/Main.java | 2 +- .../java/org/asamk/signal/commands/DaemonCommand.java | 8 ++++---- .../asamk/signal/commands/JsonRpcDispatcherCommand.java | 2 +- .../org/asamk/signal/commands/JsonRpcLocalCommand.java | 9 --------- .../org/asamk/signal/commands/ListGroupsCommand.java | 2 +- .../java/org/asamk/signal/commands/QuitGroupCommand.java | 2 +- .../java/org/asamk/signal/commands/ReceiveCommand.java | 2 +- .../java/org/asamk/signal/commands/RegisterCommand.java | 2 +- .../org/asamk/signal/commands/RemoteDeleteCommand.java | 2 +- src/main/java/org/asamk/signal/commands/SendCommand.java | 4 ++-- .../org/asamk/signal/commands/SendReactionCommand.java | 4 ++-- .../org/asamk/signal/commands/SendTypingCommand.java | 2 +- .../java/org/asamk/signal/commands/TrustCommand.java | 2 +- .../org/asamk/signal/commands/UnregisterCommand.java | 2 +- .../org/asamk/signal/commands/UpdateGroupCommand.java | 2 +- .../org/asamk/signal/commands/UpdateProfileCommand.java | 2 +- 17 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index bffbded5..c44c737c 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -117,8 +117,8 @@ public class App { var username = ns.getString("username"); - final var useDbus = ns.getBoolean("dbus"); - final var useDbusSystem = ns.getBoolean("dbus-system"); + final var useDbus = Boolean.TRUE.equals(ns.getBoolean("dbus")); + final var useDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system")); if (useDbus || useDbusSystem) { // If username is null, it will connect to the default object path initDbusClient(command, username, useDbusSystem, outputWriter); diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 26079ec6..2a95e6de 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -80,7 +80,7 @@ public class Main { return false; } - return ns.getBoolean("verbose"); + return Boolean.TRUE.equals(ns.getBoolean("verbose")); } private static void configureLogging(final boolean verbose) { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 9878de15..5045db9a 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -54,10 +54,10 @@ public class DaemonCommand implements MultiLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); + boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); DBusConnection.DBusBusType busType; - if (ns.getBoolean("system")) { + if (Boolean.TRUE.equals(ns.getBoolean("system"))) { busType = DBusConnection.DBusBusType.SYSTEM; } else { busType = DBusConnection.DBusBusType.SESSION; @@ -83,10 +83,10 @@ public class DaemonCommand implements MultiLocalCommand { public void handleCommand( final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter ) throws CommandException { - boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); + boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); DBusConnection.DBusBusType busType; - if (ns.getBoolean("system")) { + if (Boolean.TRUE.equals(ns.getBoolean("system"))) { busType = DBusConnection.DBusBusType.SYSTEM; } else { busType = DBusConnection.DBusBusType.SESSION; diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index d0e4dfec..9af67322 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -65,7 +65,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); + final boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); final var objectMapper = Util.createJsonObjectMapper(); final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter); diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java index 24b45ee8..5b926732 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java @@ -64,14 +64,5 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand> return super.getList(dest + "s"); } - - @Override - public Boolean getBoolean(String dest) { - Boolean maybeGotten = this.get(dest); - if (maybeGotten == null) { - maybeGotten = false; - } - return maybeGotten; - } } } diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 1eda53ce..fd8c4b92 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -102,7 +102,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { jsonWriter.write(jsonGroups); } else { final var writer = (PlainTextWriter) outputWriter; - boolean detailed = ns.getBoolean("detailed"); + boolean detailed = Boolean.TRUE.equals(ns.getBoolean("detailed")); for (var group : groups) { printGroupPlainText(writer, group, detailed); } diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 7635f8ae..1d6611b5 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -61,7 +61,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { } catch (NotAGroupMemberException e) { logger.info("User is not a group member"); } - if (ns.getBoolean("delete")) { + if (Boolean.TRUE.equals(ns.getBoolean("delete"))) { logger.debug("Deleting group {}", groupId); m.deleteGroup(groupId); } diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 62b3164b..4686f26d 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -147,7 +147,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { returnOnTimeout = false; timeout = 3600; } - boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); + boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); try { final var handler = outputWriter instanceof JsonWriter ? new JsonReceiveMessageHandler(m, (JsonWriter) outputWriter) : new ReceiveMessageHandler(m, (PlainTextWriter) outputWriter); diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index 96530889..af6c06ad 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -31,7 +31,7 @@ public class RegisterCommand implements RegistrationCommand { @Override public void handleCommand(final Namespace ns, final RegistrationManager m) throws CommandException { - final boolean voiceVerification = ns.getBoolean("voice"); + final boolean voiceVerification = Boolean.TRUE.equals(ns.getBoolean("voice")); final var captchaString = ns.getString("captcha"); final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", ""); diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index e515defe..c9eab95c 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -43,7 +43,7 @@ public class RemoteDeleteCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self")); final var recipientStrings = ns.getList("recipient"); final var groupIdStrings = ns.getList("group-id"); diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 1cd2e674..dba7689f 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -58,7 +58,7 @@ public class SendCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self")); final var recipientStrings = ns.getList("recipient"); final var groupIdStrings = ns.getList("group-id"); @@ -67,7 +67,7 @@ public class SendCommand implements JsonRpcLocalCommand { recipientStrings, groupIdStrings); - final var isEndSession = ns.getBoolean("end-session"); + final var isEndSession = Boolean.TRUE.equals(ns.getBoolean("end-session")); if (isEndSession) { final var singleRecipients = recipientIdentifiers.stream() .filter(r -> r instanceof RecipientIdentifier.Single) diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index a1c6c319..857f603d 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -52,7 +52,7 @@ public class SendReactionCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self")); final var recipientStrings = ns.getList("recipient"); final var groupIdStrings = ns.getList("group-id"); @@ -62,7 +62,7 @@ public class SendReactionCommand implements JsonRpcLocalCommand { groupIdStrings); final var emoji = ns.getString("emoji"); - final var isRemove = ns.getBoolean("remove"); + final var isRemove = Boolean.TRUE.equals(ns.getBoolean("remove")); final var targetAuthor = ns.getString("target-author"); final var targetTimestamp = ns.getLong("target-timestamp"); diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index cfe66770..ba062b70 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -41,7 +41,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand { ) throws CommandException { final var recipientStrings = ns.getList("recipient"); final var groupIdStrings = ns.getList("group-id"); - final var action = ns.getBoolean("stop") ? TypingAction.STOP : TypingAction.START; + final var action = Boolean.TRUE.equals(ns.getBoolean("stop")) ? TypingAction.STOP : TypingAction.START; final var recipientIdentifiers = new HashSet(); if (recipientStrings != null) { diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 9e59ad86..77fcc08a 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -39,7 +39,7 @@ public class TrustCommand implements JsonRpcLocalCommand { ) throws CommandException { var recipentString = ns.getString("recipient"); var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getSelfNumber()); - if (ns.getBoolean("trust-all-known-keys")) { + if (Boolean.TRUE.equals(ns.getBoolean("trust-all-known-keys"))) { boolean res = m.trustIdentityAllKeys(recipient); if (!res) { throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct."); diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java index 60260046..68a20375 100644 --- a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -31,7 +31,7 @@ public class UnregisterCommand implements LocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { try { - if (ns.getBoolean("delete-account")) { + if (Boolean.TRUE.equals(ns.getBoolean("delete-account"))) { m.deleteAccount(); } else { m.unregister(); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 4bbaa992..68bce2d2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -121,7 +121,7 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), localNumber); var groupRemoveAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("remove-admin"), localNumber); var groupAvatar = ns.getString("avatar"); - var groupResetLink = ns.getBoolean("reset-link"); + var groupResetLink = Boolean.TRUE.equals(ns.getBoolean("reset-link")); var groupLinkState = getGroupLinkState(ns.getString("link")); var groupExpiration = ns.getInt("expiration"); var groupAddMemberPermission = getGroupPermission(ns.getString("set-permission-add-member")); diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index f6dcb30e..9890a597 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -42,7 +42,7 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand { var about = ns.getString("about"); var aboutEmoji = ns.getString("about-emoji"); var avatarPath = ns.getString("avatar"); - boolean removeAvatar = ns.getBoolean("remove-avatar"); + boolean removeAvatar = Boolean.TRUE.equals(ns.getBoolean("remove-avatar")); Optional avatarFile = removeAvatar ? Optional.absent() From 6f5e72119e0c996f1efefecda11e33422d44a171 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 29 Sep 2021 19:38:31 +0200 Subject: [PATCH 19/26] Implement configuration handling Closes #747 --- .../org/asamk/signal/manager/Manager.java | 7 ++ .../org/asamk/signal/manager/ManagerImpl.java | 25 +++++ .../actions/SendSyncConfigurationAction.java | 20 ++++ .../configuration/ConfigurationStore.java | 93 +++++++++++++++++++ .../helper/IncomingMessageHandler.java | 13 ++- .../signal/manager/helper/SyncHelper.java | 10 ++ .../signal/manager/storage/SignalAccount.java | 26 +++++- man/signal-cli.1.adoc | 17 ++++ .../org/asamk/signal/commands/Commands.java | 1 + .../commands/UpdateConfigurationCommand.java | 55 +++++++++++ .../asamk/signal/dbus/DbusManagerImpl.java | 10 ++ 11 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java create mode 100644 src/main/java/org/asamk/signal/commands/UpdateConfigurationCommand.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 cba438f8..cd7b0335 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -98,6 +98,13 @@ public interface Manager extends Closeable { void updateAccountAttributes(String deviceName) throws IOException; + void updateConfiguration( + final Boolean readReceipts, + final Boolean unidentifiedDeliveryIndicators, + final Boolean typingIndicators, + final Boolean linkPreviews + ) throws IOException, NotMasterDeviceException; + void setProfile( String givenName, String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException; 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 de60fa50..36c131db 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -320,6 +320,31 @@ public class ManagerImpl implements Manager { account.isDiscoverableByPhoneNumber()); } + @Override + public void updateConfiguration( + final Boolean readReceipts, + final Boolean unidentifiedDeliveryIndicators, + final Boolean typingIndicators, + final Boolean linkPreviews + ) throws IOException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + if (readReceipts != null) { + account.getConfigurationStore().setReadReceipts(readReceipts); + } + if (unidentifiedDeliveryIndicators != null) { + account.getConfigurationStore().setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators); + } + if (typingIndicators != null) { + account.getConfigurationStore().setTypingIndicators(typingIndicators); + } + if (linkPreviews != null) { + account.getConfigurationStore().setLinkPreviews(linkPreviews); + } + syncHelper.sendConfigurationMessage(); + } + /** * @param givenName if null, the previous givenName will be kept * @param familyName if null, the previous familyName will be kept diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java new file mode 100644 index 00000000..0e050f0a --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncConfigurationAction implements HandleAction { + + private static final SendSyncConfigurationAction INSTANCE = new SendSyncConfigurationAction(); + + private SendSyncConfigurationAction() { + } + + public static SendSyncConfigurationAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendConfigurationMessage(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java b/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java new file mode 100644 index 00000000..e7e1b5f5 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java @@ -0,0 +1,93 @@ +package org.asamk.signal.manager.configuration; + +public class ConfigurationStore { + + private final Saver saver; + + private Boolean readReceipts; + private Boolean unidentifiedDeliveryIndicators; + private Boolean typingIndicators; + private Boolean linkPreviews; + + public ConfigurationStore(final Saver saver) { + this.saver = saver; + } + + public static ConfigurationStore fromStorage(Storage storage, Saver saver) { + final var store = new ConfigurationStore(saver); + store.readReceipts = storage.readReceipts; + store.unidentifiedDeliveryIndicators = storage.unidentifiedDeliveryIndicators; + store.typingIndicators = storage.typingIndicators; + store.linkPreviews = storage.linkPreviews; + return store; + } + + public Boolean getReadReceipts() { + return readReceipts; + } + + public void setReadReceipts(final boolean readReceipts) { + this.readReceipts = readReceipts; + saver.save(toStorage()); + } + + public Boolean getUnidentifiedDeliveryIndicators() { + return unidentifiedDeliveryIndicators; + } + + public void setUnidentifiedDeliveryIndicators(final boolean unidentifiedDeliveryIndicators) { + this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators; + saver.save(toStorage()); + } + + public Boolean getTypingIndicators() { + return typingIndicators; + } + + public void setTypingIndicators(final boolean typingIndicators) { + this.typingIndicators = typingIndicators; + saver.save(toStorage()); + } + + public Boolean getLinkPreviews() { + return linkPreviews; + } + + public void setLinkPreviews(final boolean linkPreviews) { + this.linkPreviews = linkPreviews; + saver.save(toStorage()); + } + + private Storage toStorage() { + return new Storage(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews); + } + + public static final class Storage { + + public Boolean readReceipts; + public Boolean unidentifiedDeliveryIndicators; + public Boolean typingIndicators; + public Boolean linkPreviews; + + // For deserialization + private Storage() { + } + + public Storage( + final Boolean readReceipts, + final Boolean unidentifiedDeliveryIndicators, + final Boolean typingIndicators, + final Boolean linkPreviews + ) { + this.readReceipts = readReceipts; + this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators; + this.typingIndicators = typingIndicators; + this.linkPreviews = linkPreviews; + } + } + + public interface Saver { + + void save(Storage storage); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 64e16857..dead91b0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -15,6 +15,7 @@ import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendReceiptAction; import org.asamk.signal.manager.actions.SendRetryMessageRequestAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction; +import org.asamk.signal.manager.actions.SendSyncConfigurationAction; import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; import org.asamk.signal.manager.actions.SendSyncKeysAction; @@ -271,7 +272,9 @@ public final class IncomingMessageHandler { if (rm.isKeysRequest()) { actions.add(SendSyncKeysAction.create()); } - // TODO Handle rm.isConfigurationRequest(); + if (rm.isConfigurationRequest()) { + actions.add(SendSyncConfigurationAction.create()); + } } if (syncMessage.getGroups().isPresent()) { logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring."); @@ -353,7 +356,13 @@ public final class IncomingMessageHandler { } } if (syncMessage.getConfiguration().isPresent()) { - // TODO + final var configurationMessage = syncMessage.getConfiguration().get(); + account.getConfigurationStore().setReadReceipts(configurationMessage.getReadReceipts().orNull()); + account.getConfigurationStore().setLinkPreviews(configurationMessage.getLinkPreviews().orNull()); + account.getConfigurationStore().setTypingIndicators(configurationMessage.getTypingIndicators().orNull()); + account.getConfigurationStore() + .setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators() + .orNull()); } return actions; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 6db1ca7d..e3fc7fc2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -14,6 +14,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; @@ -221,6 +222,15 @@ public class SyncHelper { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); } + public void sendConfigurationMessage() throws IOException { + final var config = account.getConfigurationStore(); + var configurationMessage = new ConfigurationMessage(Optional.fromNullable(config.getReadReceipts()), + Optional.fromNullable(config.getUnidentifiedDeliveryIndicators()), + Optional.fromNullable(config.getTypingIndicators()), + Optional.fromNullable(config.getLinkPreviews())); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage)); + } + public void handleSyncDeviceContacts(final InputStream input) throws IOException { final var s = new DeviceContactsInputStream(input); DeviceContact c; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index fd4ec597..9c51017c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.configuration.ConfigurationStore; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.storage.contacts.ContactsStore; import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore; @@ -103,6 +104,8 @@ public class SignalAccount implements Closeable { private RecipientStore recipientStore; private StickerStore stickerStore; private StickerStore.Storage stickerStoreStorage; + private ConfigurationStore configurationStore; + private ConfigurationStore.Storage configurationStoreStorage; private MessageCache messageCache; @@ -159,6 +162,7 @@ public class SignalAccount implements Closeable { account.recipientStore, account::saveGroupStore); account.stickerStore = new StickerStore(account::saveStickerStore); + account.configurationStore = new ConfigurationStore(account::saveConfigurationStore); account.registered = false; @@ -267,6 +271,7 @@ public class SignalAccount implements Closeable { account.recipientStore, account::saveGroupStore); account.stickerStore = new StickerStore(account::saveStickerStore); + account.configurationStore = new ConfigurationStore(account::saveConfigurationStore); account.recipientStore.resolveRecipientTrusted(account.getSelfAddress()); account.migrateLegacyConfigs(); @@ -491,6 +496,15 @@ public class SignalAccount implements Closeable { stickerStore = new StickerStore(this::saveStickerStore); } + if (rootNode.hasNonNull("configurationStore")) { + configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"), + ConfigurationStore.Storage.class); + configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage, + this::saveConfigurationStore); + } else { + configurationStore = new ConfigurationStore(this::saveConfigurationStore); + } + migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig; if (migratedLegacyConfig) { @@ -677,6 +691,11 @@ public class SignalAccount implements Closeable { save(); } + private void saveConfigurationStore(ConfigurationStore.Storage storage) { + this.configurationStoreStorage = storage; + save(); + } + private void save() { synchronized (fileChannel) { var rootNode = jsonProcessor.createObjectNode(); @@ -707,7 +726,8 @@ public class SignalAccount implements Closeable { profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize())) .put("registered", registered) .putPOJO("groupStore", groupStoreStorage) - .putPOJO("stickerStore", stickerStoreStorage); + .putPOJO("stickerStore", stickerStoreStorage) + .putPOJO("configurationStore", configurationStoreStorage); try { try (var output = new ByteArrayOutputStream()) { // Write to memory first to prevent corrupting the file in case of serialization errors @@ -797,6 +817,10 @@ public class SignalAccount implements Closeable { return senderKeyStore; } + public ConfigurationStore getConfigurationStore() { + return configurationStore; + } + public MessageCache getMessageCache() { return messageCache; } diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 573ade7c..9829fe00 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -113,6 +113,23 @@ Can fix problems with receiving messages. *-n* NAME, *--device-name* NAME:: Set a new device name for the main or linked device +=== updateConfiguration + +Update signal configs and sync them to linked devices. +This command only works on the main devices. + +*--read-receipts* {true,false}:: +Indicates if Signal should send read receipts. + +*--unidentified-delivery-indicators* {true,false}:: +Indicates if Signal should show unidentified delivery indicators. + +*--typing-indicators* {true,false}:: +Indicates if Signal should send/show typing indicators. + +*--link-previews* {true,false}:: +Indicates if Signal should generate link previews. + === setPin Set a registration lock pin, to prevent others from registering this number. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 5d637eee..1d6dd26d 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -39,6 +39,7 @@ public class Commands { addCommand(new UnblockCommand()); addCommand(new UnregisterCommand()); addCommand(new UpdateAccountCommand()); + addCommand(new UpdateConfigurationCommand()); addCommand(new UpdateContactCommand()); addCommand(new UpdateGroupCommand()); addCommand(new UpdateProfileCommand()); diff --git a/src/main/java/org/asamk/signal/commands/UpdateConfigurationCommand.java b/src/main/java/org/asamk/signal/commands/UpdateConfigurationCommand.java new file mode 100644 index 00000000..9ca126d0 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UpdateConfigurationCommand.java @@ -0,0 +1,55 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.OutputWriter; +import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotMasterDeviceException; + +import java.io.IOException; + +public class UpdateConfigurationCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "updateConfiguration"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Update signal configs and sync them to linked devices."); + subparser.addArgument("--read-receipts") + .type(Boolean.class) + .help("Indicates if Signal should send read receipts."); + subparser.addArgument("--unidentified-delivery-indicators") + .type(Boolean.class) + .help("Indicates if Signal should show unidentified delivery indicators."); + subparser.addArgument("--typing-indicators") + .type(Boolean.class) + .help("Indicates if Signal should send/show typing indicators."); + subparser.addArgument("--link-previews") + .type(Boolean.class) + .help("Indicates if Signal should generate link previews."); + } + + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var readReceipts = ns.getBoolean("read-receipts"); + final var unidentifiedDeliveryIndicators = ns.getBoolean("unidentified-delivery-indicators"); + final var typingIndicators = ns.getBoolean("typing-indicators"); + final var linkPreviews = ns.getBoolean("link-previews"); + try { + m.updateConfiguration(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews); + } catch (IOException e) { + throw new IOErrorException("UpdateAccount error: " + e.getMessage(), e); + } catch (NotMasterDeviceException e) { + throw new UserErrorException("This command doesn't work on linked devices."); + } + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index b9f5ae11..ea776797 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -93,6 +93,16 @@ public class DbusManagerImpl implements Manager { } } + @Override + public void updateConfiguration( + final Boolean readReceipts, + final Boolean unidentifiedDeliveryIndicators, + final Boolean typingIndicators, + final Boolean linkPreviews + ) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public void setProfile( final String givenName, From 9839be48f3ff80456ddec3ad28cf0b583bf80226 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 1 Oct 2021 17:52:33 +0200 Subject: [PATCH 20/26] Extract configurationStore variable --- .../java/org/asamk/signal/manager/ManagerImpl.java | 10 ++++++---- .../manager/helper/IncomingMessageHandler.java | 12 ++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) 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 36c131db..86ec34c1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -330,17 +330,19 @@ public class ManagerImpl implements Manager { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } + + final var configurationStore = account.getConfigurationStore(); if (readReceipts != null) { - account.getConfigurationStore().setReadReceipts(readReceipts); + configurationStore.setReadReceipts(readReceipts); } if (unidentifiedDeliveryIndicators != null) { - account.getConfigurationStore().setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators); + configurationStore.setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators); } if (typingIndicators != null) { - account.getConfigurationStore().setTypingIndicators(typingIndicators); + configurationStore.setTypingIndicators(typingIndicators); } if (linkPreviews != null) { - account.getConfigurationStore().setLinkPreviews(linkPreviews); + configurationStore.setLinkPreviews(linkPreviews); } syncHelper.sendConfigurationMessage(); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index dead91b0..16f47d3c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -357,12 +357,12 @@ public final class IncomingMessageHandler { } if (syncMessage.getConfiguration().isPresent()) { final var configurationMessage = syncMessage.getConfiguration().get(); - account.getConfigurationStore().setReadReceipts(configurationMessage.getReadReceipts().orNull()); - account.getConfigurationStore().setLinkPreviews(configurationMessage.getLinkPreviews().orNull()); - account.getConfigurationStore().setTypingIndicators(configurationMessage.getTypingIndicators().orNull()); - account.getConfigurationStore() - .setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators() - .orNull()); + final var configurationStore = account.getConfigurationStore(); + configurationStore.setReadReceipts(configurationMessage.getReadReceipts().orNull()); + configurationStore.setLinkPreviews(configurationMessage.getLinkPreviews().orNull()); + configurationStore.setTypingIndicators(configurationMessage.getTypingIndicators().orNull()); + configurationStore.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators() + .orNull()); } return actions; } From 1548ce9c795662a0dcd6666415c2ecc0a5a88852 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Oct 2021 17:16:08 +0200 Subject: [PATCH 21/26] Add helper classes for exporting dbus properties --- .../dbus/DbusInterfacePropertiesHandler.java | 46 +++++++++++++ .../org/asamk/signal/dbus/DbusProperties.java | 66 +++++++++++++++++++ .../org/asamk/signal/dbus/DbusProperty.java | 35 ++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/main/java/org/asamk/signal/dbus/DbusInterfacePropertiesHandler.java create mode 100644 src/main/java/org/asamk/signal/dbus/DbusProperties.java create mode 100644 src/main/java/org/asamk/signal/dbus/DbusProperty.java diff --git a/src/main/java/org/asamk/signal/dbus/DbusInterfacePropertiesHandler.java b/src/main/java/org/asamk/signal/dbus/DbusInterfacePropertiesHandler.java new file mode 100644 index 00000000..d3c2ca83 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusInterfacePropertiesHandler.java @@ -0,0 +1,46 @@ +package org.asamk.signal.dbus; + +import org.asamk.Signal; + +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class DbusInterfacePropertiesHandler { + + private final String interfaceName; + private final List> properties; + + public DbusInterfacePropertiesHandler( + final String interfaceName, final List> properties + ) { + this.interfaceName = interfaceName; + this.properties = properties; + } + + public String getInterfaceName() { + return interfaceName; + } + + @SuppressWarnings("unchecked") + private DbusProperty findProperty(String propertyName) { + final var property = properties.stream().filter(p -> p.getName().equals(propertyName)).findFirst(); + if (property.isEmpty()) { + throw new Signal.Error.Failure("Property not found"); + } + return (DbusProperty) property.get(); + } + + Consumer getSetter(String propertyName) { + return this.findProperty(propertyName).getSetter(); + } + + Supplier getGetter(String propertyName) { + return this.findProperty(propertyName).getGetter(); + } + + Collection> getProperties() { + return properties; + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusProperties.java b/src/main/java/org/asamk/signal/dbus/DbusProperties.java new file mode 100644 index 00000000..37cc35e3 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusProperties.java @@ -0,0 +1,66 @@ +package org.asamk.signal.dbus; + +import org.asamk.Signal; +import org.freedesktop.dbus.interfaces.Properties; +import org.freedesktop.dbus.types.Variant; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public abstract class DbusProperties implements Properties { + + private final List handlers = new ArrayList<>(); + + protected void addPropertiesHandler(DbusInterfacePropertiesHandler handler) { + this.handlers.add(handler); + } + + DbusInterfacePropertiesHandler getHandler(String interfaceName) { + final var handler = getHandlerOptional(interfaceName); + if (handler.isEmpty()) { + throw new Signal.Error.Failure("Property not found"); + } + return handler.get(); + } + + private java.util.Optional getHandlerOptional(final String interfaceName) { + return handlers.stream().filter(h -> h.getInterfaceName().equals(interfaceName)).findFirst(); + } + + @Override + @SuppressWarnings("unchecked") + public A Get(final String interface_name, final String property_name) { + final var handler = getHandler(interface_name); + final var getter = handler.getGetter(property_name); + if (getter == null) { + throw new Signal.Error.Failure("Property not found"); + } + return (A) getter.get(); + } + + @Override + public void Set(final String interface_name, final String property_name, final A value) { + final var handler = getHandler(interface_name); + final var setter = handler.getSetter(property_name); + if (setter == null) { + throw new Signal.Error.Failure("Property not found"); + } + setter.accept(value); + } + + @Override + public Map> GetAll(final String interface_name) { + final var handler = getHandlerOptional(interface_name); + if (handler.isEmpty()) { + return Map.of(); + } + + return handler.get() + .getProperties() + .stream() + .filter(p -> p.getGetter() != null) + .collect(Collectors.toMap(DbusProperty::getName, p -> new Variant<>(p.getGetter().get()))); + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusProperty.java b/src/main/java/org/asamk/signal/dbus/DbusProperty.java new file mode 100644 index 00000000..e0557786 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusProperty.java @@ -0,0 +1,35 @@ +package org.asamk.signal.dbus; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class DbusProperty { + + private final String name; + private final Supplier getter; + private final Consumer setter; + + public DbusProperty(final String name, final Supplier getter, final Consumer setter) { + this.name = name; + this.getter = getter; + this.setter = setter; + } + + public DbusProperty(final String name, final Supplier getter) { + this.name = name; + this.getter = getter; + this.setter = null; + } + + public String getName() { + return name; + } + + public Consumer getSetter() { + return setter; + } + + public Supplier getGetter() { + return getter; + } +} From 778adacb80bae7d6ecc1d70fa87f9217c7bc1c71 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Oct 2021 18:04:30 +0200 Subject: [PATCH 22/26] Refactor dbus linked devices interface Export a separate dbus object for each device --- .../org/asamk/signal/manager/Manager.java | 2 +- .../org/asamk/signal/manager/ManagerImpl.java | 2 +- src/main/java/org/asamk/Signal.java | 18 ++- src/main/java/org/asamk/signal/App.java | 2 +- .../asamk/signal/commands/DaemonCommand.java | 10 +- .../signal/commands/RemoveDeviceCommand.java | 4 +- .../asamk/signal/dbus/DbusManagerImpl.java | 38 ++++-- .../org/asamk/signal/dbus/DbusSignalImpl.java | 119 ++++++++++++++---- 8 files changed, 157 insertions(+), 38 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 cd7b0335..7a421966 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -117,7 +117,7 @@ public interface Manager extends Closeable { List getLinkedDevices() throws IOException; - void removeLinkedDevices(int deviceId) throws IOException; + void removeLinkedDevices(long deviceId) throws IOException; void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException; 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 86ec34c1..6a039c69 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -414,7 +414,7 @@ public class ManagerImpl implements Manager { } @Override - public void removeLinkedDevices(int deviceId) throws IOException { + public void removeLinkedDevices(long deviceId) throws IOException { dependencies.getAccountManager().removeDevice(deviceId); var devices = dependencies.getAccountManager().getDevices(); account.setMultiDevice(devices.size() > 1); diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index cc521f6d..b8800085 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,8 +1,11 @@ package org.asamk; +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.messages.DBusSignal; import java.util.List; @@ -97,11 +100,11 @@ public interface Signal extends DBusInterface { void addDevice(String uri) throws Error.InvalidUri; - void removeDevice(int deviceId) throws Error.Failure; + DBusPath getDevice(long deviceId); - List listDevices() throws Error.Failure; + List listDevices() throws Error.Failure; - void updateDeviceName(String deviceName) throws Error.Failure; + DBusPath getThisDevice(); void updateProfile( String givenName, @@ -255,6 +258,15 @@ public interface Signal extends DBusInterface { } } + @DBusProperty(name = "Id", type = Integer.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) + interface Device extends DBusInterface, Properties { + + void removeDevice() throws Error.Failure; + } + interface Error { class AttachmentInvalid extends DBusExecutionException { diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index c44c737c..3d35ff8f 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -349,7 +349,7 @@ public class App { ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn, outputWriter); } else if (command instanceof LocalCommand) { try { - ((LocalCommand) command).handleCommand(ns, new DbusManagerImpl(ts), outputWriter); + ((LocalCommand) command).handleCommand(ns, new DbusManagerImpl(ts, dBusConn), outputWriter); } catch (UnsupportedOperationException e) { throw new UserErrorException("Command is not yet implemented via dbus", e); } catch (DBusExecutionException e) { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 5045db9a..02063b87 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -120,7 +120,10 @@ public class DaemonCommand implements MultiLocalCommand { private Thread run( DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments ) throws DBusException { - conn.exportObject(new DbusSignalImpl(m, objectPath)); + final var signal = new DbusSignalImpl(m, conn, objectPath); + conn.exportObject(signal); + final var initThread = new Thread(signal::initObjects); + initThread.start(); logger.info("Exported dbus object: " + objectPath); @@ -136,6 +139,11 @@ public class DaemonCommand implements MultiLocalCommand { logger.warn("Receiving messages failed, retrying", e); } } + try { + initThread.join(); + } catch (InterruptedException ignored) { + } + signal.close(); }); thread.start(); diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java index d67cc5ea..4fcad79d 100644 --- a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -21,7 +21,7 @@ public class RemoveDeviceCommand implements JsonRpcLocalCommand { public void attachToSubparser(final Subparser subparser) { subparser.help("Remove a linked device."); subparser.addArgument("-d", "--device-id", "--deviceId") - .type(int.class) + .type(long.class) .required(true) .help("Specify the device you want to remove. Use listDevices to see the deviceIds."); } @@ -31,7 +31,7 @@ public class RemoveDeviceCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { try { - int deviceId = ns.getInt("device-id"); + final var deviceId = ns.getLong("device-id"); m.removeLinkedDevices(deviceId); } catch (IOException e) { throw new IOErrorException("Error while removing device: " + e.getMessage(), e); diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index ea776797..3124a5b0 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -1,6 +1,7 @@ package org.asamk.signal.dbus; import org.asamk.Signal; +import org.asamk.signal.DbusConfig; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -25,6 +26,10 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; 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.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; @@ -58,9 +63,11 @@ import java.util.stream.Collectors; public class DbusManagerImpl implements Manager { private final Signal signal; + private final DBusConnection connection; - public DbusManagerImpl(final Signal signal) { + public DbusManagerImpl(final Signal signal, DBusConnection connection) { this.signal = signal; + this.connection = connection; } @Override @@ -89,7 +96,8 @@ public class DbusManagerImpl implements Manager { @Override public void updateAccountAttributes(final String deviceName) throws IOException { if (deviceName != null) { - signal.updateDeviceName(deviceName); + final var devicePath = signal.getThisDevice(); + getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName); } } @@ -136,15 +144,21 @@ public class DbusManagerImpl implements Manager { @Override public List getLinkedDevices() throws IOException { - return signal.listDevices() - .stream() - .map(name -> new Device(-1, name, 0, 0, false)) - .collect(Collectors.toList()); + 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 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)); + }).collect(Collectors.toList()); } @Override - public void removeLinkedDevices(final int deviceId) throws IOException { - signal.removeDevice(deviceId); + public void removeLinkedDevices(final long deviceId) throws IOException { + final var devicePath = signal.getDevice(deviceId); + getRemoteObject(devicePath, Signal.Device.class).removeDevice(); } @Override @@ -494,4 +508,12 @@ public class DbusManagerImpl implements Manager { private String emptyIfNull(final String string) { return string == null ? "" : string; } + + private T getRemoteObject(final DBusPath devicePath, final Class type) { + try { + return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type); + } catch (DBusException e) { + throw new AssertionError(e); + } + } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 1a4fdc10..ab9c89b2 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -7,7 +7,6 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; -import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -21,6 +20,9 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.util.ErrorUtils; +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.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; @@ -49,16 +51,24 @@ import java.util.stream.Stream; public class DbusSignalImpl implements Signal { private final Manager m; + private final DBusConnection connection; private final String objectPath; - public DbusSignalImpl(final Manager m, final String objectPath) { + private DBusPath thisDevice; + private final List devices = new ArrayList<>(); + + public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) { this.m = m; + this.connection = connection; this.objectPath = objectPath; } - @Override - public boolean isRemote() { - return false; + public void initObjects() { + updateDevices(); + } + + public void close() { + unExportDevices(); } @Override @@ -85,33 +95,51 @@ public class DbusSignalImpl implements Signal { } @Override - public void removeDevice(int deviceId) { - try { - m.removeLinkedDevices(deviceId); - } catch (IOException e) { - throw new Error.Failure(e.getClass().getSimpleName() + ": Error while removing device: " + e.getMessage()); - } + public DBusPath getDevice(long deviceId) { + updateDevices(); + return new DBusPath(getDeviceObjectPath(objectPath, deviceId)); } @Override - public List listDevices() { - List devices; + public List listDevices() { + updateDevices(); + return this.devices; + } + + private void updateDevices() { + List linkedDevices; try { - devices = m.getLinkedDevices(); + linkedDevices = m.getLinkedDevices(); } catch (IOException | Error.Failure e) { throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); } - return devices.stream().map(d -> d.getName() == null ? "" : d.getName()).collect(Collectors.toList()); + 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 void updateDeviceName(String deviceName) { - try { - m.updateAccountAttributes(deviceName); - } catch (IOException | Signal.Error.Failure e) { - throw new Error.Failure("UpdateAccount error: " + e.getMessage()); - } + public DBusPath getThisDevice() { + updateDevices(); + return thisDevice; } @Override @@ -759,4 +787,53 @@ public class DbusSignalImpl implements Signal { private String nullIfEmpty(final String name) { return name.isEmpty() ? null : name; } + + private static String getDeviceObjectPath(String basePath, long deviceId) { + return basePath + "/Devices/" + deviceId; + } + + 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<>("Created", device::getCreated), + new DbusProperty<>("LastSeen", device::getLastSeen)))); + this.device = device; + } + + @Override + public String getObjectPath() { + return getDeviceObjectPath(objectPath, device.getId()); + } + + @Override + public void removeDevice() throws Error.Failure { + try { + m.removeLinkedDevices(device.getId()); + updateDevices(); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + private void setDeviceName(String name) { + if (!device.isThisDevice()) { + throw new Error.Failure("Only the name of this device can be changed"); + } + try { + m.updateAccountAttributes(name); + // update device list + updateDevices(); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + } } From 8b83992e95dbd0f888ec71ee751613a77ec00820 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Oct 2021 18:40:36 +0200 Subject: [PATCH 23/26] Don't repeatedly try to refetch group info if permission was denied i.e. if the user is no longer a member of that group --- .../signal/manager/helper/GroupHelper.java | 23 ++++++++--- .../signal/manager/helper/GroupV2Helper.java | 10 ++++- .../manager/storage/groups/GroupInfoV2.java | 20 +++++++++- .../manager/storage/groups/GroupStore.java | 38 ++++--------------- 4 files changed, 54 insertions(+), 37 deletions(-) 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 3ddd6edd..62f4f111 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 @@ -145,7 +145,10 @@ public class GroupHelper { groupMasterKey); } if (group == null) { - group = groupV2Helper.getDecryptedGroup(groupSecretParams); + try { + group = groupV2Helper.getDecryptedGroup(groupSecretParams); + } catch (NotAGroupMemberException ignored) { + } } if (group != null) { storeProfileKeysFromMembers(group); @@ -348,10 +351,20 @@ public class GroupHelper { private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { final var group = account.getGroupStore().getGroup(groupId); - if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); - ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), recipientResolver); - account.getGroupStore().updateGroup(group); + if (group instanceof GroupInfoV2) { + final var groupInfoV2 = (GroupInfoV2) group; + if (forceUpdate || (!groupInfoV2.isPermissionDenied() && groupInfoV2.getGroup() == null)) { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); + DecryptedGroup decryptedGroup; + try { + decryptedGroup = groupV2Helper.getDecryptedGroup(groupSecretParams); + } catch (NotAGroupMemberException e) { + groupInfoV2.setPermissionDenied(true); + decryptedGroup = null; + } + groupInfoV2.setGroup(decryptedGroup, recipientResolver); + account.getGroupStore().updateGroup(group); + } } return group; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 3187fca1..746af2f9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.groups.GroupLinkPassword; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; @@ -35,6 +36,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.File; @@ -78,10 +80,16 @@ public class GroupV2Helper { this.addressResolver = addressResolver; } - public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { + public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException { try { final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); + } catch (NonSuccessfulResponseCodeException e) { + if (e.getCode() == 403) { + throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); + } + logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); + return null; } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); return null; 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 f86dcb04..a06b83df 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 @@ -22,16 +22,23 @@ public class GroupInfoV2 extends GroupInfo { private boolean blocked; private DecryptedGroup group; // stored as a file with hexadecimal groupId as name private RecipientResolver recipientResolver; + private boolean permissionDenied; public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) { this.groupId = groupId; this.masterKey = masterKey; } - public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) { + public GroupInfoV2( + final GroupIdV2 groupId, + final GroupMasterKey masterKey, + final boolean blocked, + final boolean permissionDenied + ) { this.groupId = groupId; this.masterKey = masterKey; this.blocked = blocked; + this.permissionDenied = permissionDenied; } @Override @@ -44,6 +51,9 @@ public class GroupInfoV2 extends GroupInfo { } public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) { + if (group != null) { + this.permissionDenied = false; + } this.group = group; this.recipientResolver = recipientResolver; } @@ -151,4 +161,12 @@ public class GroupInfoV2 extends GroupInfo { public boolean isAnnouncementGroup() { return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED; } + + public void setPermissionDenied(final boolean permissionDenied) { + this.permissionDenied = permissionDenied; + } + + public boolean isPermissionDenied() { + return permissionDenied; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index 4adc413a..fe8f85a6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -104,7 +104,7 @@ public class GroupStore { throw new AssertionError("Invalid master key for group " + groupId.toBase64()); } - return new GroupInfoV2(groupId, masterKey, g2.blocked); + return new GroupInfoV2(groupId, masterKey, g2.blocked, g2.permissionDenied); }).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g)); return new GroupStore(groupCachePath, groups, recipientResolver, saver); @@ -268,13 +268,13 @@ public class GroupStore { final var g2 = (GroupInfoV2) g; return new Storage.GroupV2(g2.getGroupId().toBase64(), Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()), - g2.isBlocked()); + g2.isBlocked(), + g2.isPermissionDenied()); }).collect(Collectors.toList())); } public static class Storage { - // @JsonSerialize(using = GroupsSerializer.class) @JsonDeserialize(using = GroupsDeserializer.class) public List groups; @@ -408,46 +408,24 @@ public class GroupStore { public String groupId; public String masterKey; public boolean blocked; + public boolean permissionDenied; // For deserialization private GroupV2() { } - public GroupV2(final String groupId, final String masterKey, final boolean blocked) { + public GroupV2( + final String groupId, final String masterKey, final boolean blocked, final boolean permissionDenied + ) { this.groupId = groupId; this.masterKey = masterKey; this.blocked = blocked; + this.permissionDenied = permissionDenied; } } } - // private static class GroupsSerializer extends JsonSerializer> { -// -// @Override -// public void serialize( -// final List groups, final JsonGenerator jgen, final SerializerProvider provider -// ) throws IOException { -// jgen.writeStartArray(groups.size()); -// for (var group : groups) { -// if (group instanceof GroupInfoV1) { -// jgen.writeObject(group); -// } else if (group instanceof GroupInfoV2) { -// final var groupV2 = (GroupInfoV2) group; -// jgen.writeStartObject(); -// jgen.writeStringField("groupId", groupV2.getGroupId().toBase64()); -// jgen.writeStringField("masterKey", -// Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize())); -// jgen.writeBooleanField("blocked", groupV2.isBlocked()); -// jgen.writeEndObject(); -// } else { -// throw new AssertionError("Unknown group version"); -// } -// } -// jgen.writeEndArray(); -// } -// } -// private static class GroupsDeserializer extends JsonDeserializer> { @Override From 76ceac4d543d0682335758c77971f1c174107e63 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Oct 2021 13:14:43 +0200 Subject: [PATCH 24/26] Read configurations from storage --- .../java/org/asamk/signal/manager/helper/StorageHelper.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index 4caab519..63e6ca59 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -188,6 +188,12 @@ public class StorageHelper { return; } + account.getConfigurationStore().setReadReceipts(accountRecord.isReadReceiptsEnabled()); + account.getConfigurationStore().setTypingIndicators(accountRecord.isTypingIndicatorsEnabled()); + account.getConfigurationStore() + .setUnidentifiedDeliveryIndicators(accountRecord.isSealedSenderIndicatorsEnabled()); + account.getConfigurationStore().setLinkPreviews(accountRecord.isLinkPreviewsEnabled()); + if (accountRecord.getProfileKey().isPresent()) { try { account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get())); From 0709c0caf8d66b346eb82c8ecfb6f91049993f11 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Oct 2021 13:12:48 +0200 Subject: [PATCH 25/26] Update libsignal-service-java --- lib/build.gradle.kts | 2 +- .../java/org/asamk/signal/manager/config/LiveConfig.java | 3 +++ .../org/asamk/signal/manager/config/SandboxConfig.java | 3 +++ .../org/asamk/signal/manager/config/ServiceConfig.java | 8 +++++++- .../org/asamk/signal/manager/helper/ProfileHelper.java | 4 +++- .../org/asamk/signal/manager/helper/StorageHelper.java | 4 ++++ 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index dc6c910e..6e528805 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_27") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_28") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 7762a4cb..177f6697 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -7,6 +7,7 @@ import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; @@ -38,6 +39,7 @@ class LiveConfig { private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api.directory.signal.org"; private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org"; private final static String STORAGE_URL = "https://storage.signal.org"; + private final static String SIGNAL_CDSH_URL = ""; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); private final static Optional dns = Optional.absent(); @@ -58,6 +60,7 @@ class LiveConfig { TRUST_STORE)}, new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, + new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)}, interceptors, dns, proxy, diff --git a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java index bedec52c..d643f10a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java @@ -7,6 +7,7 @@ import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; @@ -38,6 +39,7 @@ class SandboxConfig { private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api-staging.directory.signal.org"; private final static String SIGNAL_KEY_BACKUP_URL = "https://api-staging.backup.signal.org"; private final static String STORAGE_URL = "https://storage-staging.signal.org"; + private final static String SIGNAL_CDSH_URL = "https://cdsh.staging.signal.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); private final static Optional dns = Optional.absent(); @@ -58,6 +60,7 @@ class SandboxConfig { TRUST_STORE)}, new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, + new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)}, interceptors, dns, proxy, diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 3677bba1..a9a08d93 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -39,7 +39,13 @@ public class ServiceConfig { logger.warn("Failed to call libzkgroup: {}", e.getMessage()); zkGroupAvailable = false; } - capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true); + capabilities = new AccountAttributes.Capabilities(false, + zkGroupAvailable, + false, + zkGroupAvailable, + true, + true, + false); try { TrustStore contactTrustStore = new IasTrustStore(); 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 d4f8ae5d..46c83e9d 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 @@ -32,6 +32,7 @@ import java.nio.file.Files; import java.util.Base64; import java.util.Date; import java.util.HashSet; +import java.util.List; import java.util.Set; import io.reactivex.rxjava3.core.Single; @@ -136,7 +137,8 @@ public final class ProfileHelper { newProfile.getAbout() == null ? "" : newProfile.getAbout(), newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), Optional.absent(), - streamDetails); + streamDetails, + List.of(/* TODO */)); } if (avatar != null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index 63e6ca59..f76c95fb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -188,6 +188,10 @@ public class StorageHelper { return; } + if (!accountRecord.getE164().equals(account.getUsername())) { + // TODO implement changed number handling + } + account.getConfigurationStore().setReadReceipts(accountRecord.isReadReceiptsEnabled()); account.getConfigurationStore().setTypingIndicators(accountRecord.isTypingIndicatorsEnabled()); account.getConfigurationStore() From 26594dd0eed44225d7d4a17571597a81e4e3b58a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Oct 2021 16:17:58 +0200 Subject: [PATCH 26/26] Retrieve self profile from storage --- .../org/asamk/signal/manager/ManagerImpl.java | 2 +- .../signal/manager/RegistrationManager.java | 6 +- .../signal/manager/helper/ProfileHelper.java | 68 ++++++++++++++----- .../signal/manager/helper/StorageHelper.java | 23 ++++++- .../signal/manager/storage/SignalAccount.java | 1 + .../manager/storage/recipients/Profile.java | 16 +++++ .../storage/recipients/RecipientStore.java | 5 ++ .../signal/manager/util/ProfileUtils.java | 1 + 8 files changed, 99 insertions(+), 23 deletions(-) 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 6a039c69..0fd1eb33 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -209,7 +209,7 @@ public class ManagerImpl implements Manager { avatarStore, this::resolveSignalServiceAddress, account.getRecipientStore()); - this.storageHelper = new StorageHelper(account, dependencies, groupHelper); + this.storageHelper = new StorageHelper(account, dependencies, groupHelper, profileHelper); this.contactHelper = new ContactHelper(account); this.syncHelper = new SyncHelper(account, attachmentHelper, diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index ff94c19b..c42782f7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -183,15 +183,15 @@ public class RegistrationManager implements Closeable { account = null; m.refreshPreKeys(); + if (response.isStorageCapable()) { + m.retrieveRemoteStorage(); + } // Set an initial empty profile so user can be added to groups try { m.setProfile(null, null, null, null, null); } catch (NoClassDefFoundError e) { logger.warn("Failed to set default profile: {}", e.getMessage()); } - if (response.isStorageCapable()) { - m.retrieveRemoteStorage(); - } final var result = m; m = null; 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 46c83e9d..e24d41fa 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 @@ -33,6 +33,7 @@ import java.util.Base64; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import io.reactivex.rxjava3.core.Single; @@ -110,6 +111,17 @@ public final class ProfileHelper { */ public void setProfile( String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException { + setProfile(true, givenName, familyName, about, aboutEmoji, avatar); + } + + public void setProfile( + boolean uploadProfile, + String givenName, + final String familyName, + String about, + String aboutEmoji, + Optional avatar ) throws IOException { var profile = getRecipientProfile(account.getSelfRecipientId()); var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); @@ -127,18 +139,22 @@ public final class ProfileHelper { } var newProfile = builder.build(); - try (final var streamDetails = avatar == null - ? avatarStore.retrieveProfileAvatar(account.getSelfAddress()) - : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { - dependencies.getAccountManager() - .setVersionedProfile(account.getUuid(), - account.getProfileKey(), - newProfile.getInternalServiceName(), - newProfile.getAbout() == null ? "" : newProfile.getAbout(), - newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), - Optional.absent(), - streamDetails, - List.of(/* TODO */)); + if (uploadProfile) { + try (final var streamDetails = avatar == null + ? avatarStore.retrieveProfileAvatar(account.getSelfAddress()) + : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { + final var avatarPath = dependencies.getAccountManager() + .setVersionedProfile(account.getUuid(), + account.getProfileKey(), + newProfile.getInternalServiceName(), + newProfile.getAbout() == null ? "" : newProfile.getAbout(), + newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), + Optional.absent(), + streamDetails, + List.of(/* TODO */)); + builder.withAvatarUrlPath(avatarPath.orNull()); + newProfile = builder.build(); + } } if (avatar != null) { @@ -197,6 +213,7 @@ public final class ProfileHelper { null, null, null, + null, ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), ProfileUtils.getCapabilities(encryptedProfile)); } @@ -242,15 +259,23 @@ public final class ProfileHelper { private Profile decryptProfileAndDownloadAvatar( final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile ) { - if (encryptedProfile.getAvatar() != null) { - downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId), - encryptedProfile.getAvatar(), - profileKey); - } + final var avatarPath = encryptedProfile.getAvatar(); + downloadProfileAvatar(recipientId, avatarPath, profileKey); return ProfileUtils.decryptProfile(profileKey, encryptedProfile); } + public void downloadProfileAvatar( + final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey + ) { + var profile = account.getProfileStore().getProfile(recipientId); + if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) { + downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId), avatarPath, profileKey); + var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); + account.getProfileStore().storeProfile(recipientId, builder.withAvatarUrlPath(avatarPath).build()); + } + } + private ProfileAndCredential retrieveProfileSync( RecipientId recipientId, SignalServiceProfile.RequestType requestType ) throws IOException { @@ -310,6 +335,15 @@ public final class ProfileHelper { private void downloadProfileAvatar( SignalServiceAddress address, String avatarPath, ProfileKey profileKey ) { + if (avatarPath == null) { + try { + avatarStore.deleteProfileAvatar(address); + } catch (IOException e) { + logger.warn("Failed to delete local profile avatar, ignoring: {}", e.getMessage()); + } + return; + } + try { avatarStore.storeProfileAvatar(address, outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index f76c95fb..b68e65b4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -32,13 +32,18 @@ public class StorageHelper { private final SignalAccount account; private final SignalDependencies dependencies; private final GroupHelper groupHelper; + private final ProfileHelper profileHelper; public StorageHelper( - final SignalAccount account, final SignalDependencies dependencies, final GroupHelper groupHelper + final SignalAccount account, + final SignalDependencies dependencies, + final GroupHelper groupHelper, + final ProfileHelper profileHelper ) { this.account = account; this.dependencies = dependencies; this.groupHelper = groupHelper; + this.profileHelper = profileHelper; } public void readDataFromStorage() throws IOException { @@ -199,12 +204,26 @@ public class StorageHelper { account.getConfigurationStore().setLinkPreviews(accountRecord.isLinkPreviewsEnabled()); if (accountRecord.getProfileKey().isPresent()) { + ProfileKey profileKey; try { - account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get())); + profileKey = new ProfileKey(accountRecord.getProfileKey().get()); } catch (InvalidInputException e) { logger.warn("Received invalid profile key from storage"); + profileKey = null; + } + if (profileKey != null) { + account.setProfileKey(profileKey); + final var avatarPath = accountRecord.getAvatarUrlPath().orNull(); + profileHelper.downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, profileKey); } } + + profileHelper.setProfile(false, + accountRecord.getGivenName().orNull(), + accountRecord.getFamilyName().orNull(), + null, + null, + null); } private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 9c51017c..5bb9fdeb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -631,6 +631,7 @@ public class SignalAccount implements Closeable { profile.getFamilyName(), profile.getAbout(), profile.getAboutEmoji(), + null, profile.isUnrestrictedUnidentifiedAccess() ? Profile.UnidentifiedAccessMode.UNRESTRICTED : profile.getUnidentifiedAccess() != null diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java index d61a81b5..c6ba5c92 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java @@ -17,6 +17,8 @@ public class Profile { private final String aboutEmoji; + private final String avatarUrlPath; + private final UnidentifiedAccessMode unidentifiedAccessMode; private final Set capabilities; @@ -27,6 +29,7 @@ public class Profile { final String familyName, final String about, final String aboutEmoji, + final String avatarUrlPath, final UnidentifiedAccessMode unidentifiedAccessMode, final Set capabilities ) { @@ -35,6 +38,7 @@ public class Profile { this.familyName = familyName; this.about = about; this.aboutEmoji = aboutEmoji; + this.avatarUrlPath = avatarUrlPath; this.unidentifiedAccessMode = unidentifiedAccessMode; this.capabilities = capabilities; } @@ -45,6 +49,7 @@ public class Profile { familyName = builder.familyName; about = builder.about; aboutEmoji = builder.aboutEmoji; + avatarUrlPath = builder.avatarUrlPath; unidentifiedAccessMode = builder.unidentifiedAccessMode; capabilities = builder.capabilities; } @@ -60,6 +65,7 @@ public class Profile { builder.familyName = copy.getFamilyName(); builder.about = copy.getAbout(); builder.aboutEmoji = copy.getAboutEmoji(); + builder.avatarUrlPath = copy.getAvatarUrlPath(); builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode(); builder.capabilities = copy.getCapabilities(); return builder; @@ -107,6 +113,10 @@ public class Profile { return aboutEmoji; } + public String getAvatarUrlPath() { + return avatarUrlPath; + } + public UnidentifiedAccessMode getUnidentifiedAccessMode() { return unidentifiedAccessMode; } @@ -152,6 +162,7 @@ public class Profile { private String familyName; private String about; private String aboutEmoji; + private String avatarUrlPath; private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; private Set capabilities = Collections.emptySet(); private long lastUpdateTimestamp = 0; @@ -179,6 +190,11 @@ public class Profile { return this; } + public Builder withAvatarUrlPath(final String val) { + avatarUrlPath = val; + return this; + } + public Builder withUnidentifiedAccessMode(final UnidentifiedAccessMode val) { unidentifiedAccessMode = val; return this; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index bace6a6b..16302692 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -89,6 +89,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile r.profile.familyName, r.profile.about, r.profile.aboutEmoji, + r.profile.avatarUrlPath, Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode), r.profile.capabilities.stream() .map(Profile.Capability::valueOfOrNull) @@ -445,6 +446,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile recipient.getProfile().getFamilyName(), recipient.getProfile().getAbout(), recipient.getProfile().getAboutEmoji(), + recipient.getProfile().getAvatarUrlPath(), recipient.getProfile().getUnidentifiedAccessMode().name(), recipient.getProfile() .getCapabilities() @@ -558,6 +560,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile public String familyName; public String about; public String aboutEmoji; + public String avatarUrlPath; public String unidentifiedAccessMode; public Set capabilities; @@ -571,6 +574,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile final String familyName, final String about, final String aboutEmoji, + final String avatarUrlPath, final String unidentifiedAccessMode, final Set capabilities ) { @@ -579,6 +583,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile this.familyName = familyName; this.about = about; this.aboutEmoji = aboutEmoji; + this.avatarUrlPath = avatarUrlPath; this.unidentifiedAccessMode = unidentifiedAccessMode; this.capabilities = capabilities; } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index 7ceb07f6..c1b1183c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -27,6 +27,7 @@ public class ProfileUtils { nameParts.second(), about, aboutEmoji, + encryptedProfile.getAvatar(), getUnidentifiedAccessMode(encryptedProfile, profileCipher), getCapabilities(encryptedProfile)); } catch (InvalidCiphertextException e) {