From: John Freed Date: Thu, 14 Oct 2021 13:24:07 +0000 (+0200) Subject: Merge branch master into dbus_updateConfiguration X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/09da3aae62d34de33b73dc53870090c23efe078a?ds=inline;hp=-c Merge branch master into dbus_updateConfiguration --- 09da3aae62d34de33b73dc53870090c23efe078a diff --combined lib/src/main/java/org/asamk/signal/manager/Manager.java index 943b5e75,733e3dcc..93afd9a6 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@@ -8,13 -8,12 +8,12 @@@ import org.asamk.signal.manager.api.Rec import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; + import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; - import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; - import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@@ -23,7 -22,6 +22,6 @@@ import org.asamk.signal.manager.storage 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; @@@ -105,8 -103,6 +103,8 @@@ public interface Manager extends Closea final Boolean linkPreviews ) throws IOException, NotMasterDeviceException; + List getConfiguration() throws IOException, NotMasterDeviceException; + void setProfile( String givenName, String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException; @@@ -140,20 -136,7 +138,7 @@@ ) throws IOException, AttachmentInvalidException; SendGroupMessageResults updateGroup( - GroupId groupId, - String name, - String description, - Set members, - Set removeMembers, - Set admins, - Set removeAdmins, - boolean resetGroupLink, - GroupLinkState groupLinkState, - GroupPermission addMemberPermission, - GroupPermission editDetailsPermission, - File avatarFile, - Integer expirationTimer, - Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException; Pair joinGroup( @@@ -200,7 -183,7 +185,7 @@@ void setGroupBlocked( GroupId groupId, boolean blocked - ) throws GroupNotFoundException, IOException; + ) throws GroupNotFoundException, IOException, NotMasterDeviceException; void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer @@@ -244,8 -227,6 +229,6 @@@ boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient); - String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); @Override diff --combined lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index b8414329,d2ffaaab..a4121e29 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@@ -25,13 -25,12 +25,12 @@@ import org.asamk.signal.manager.api.Rec import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; + import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; - import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; - import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@@ -39,6 -38,7 +38,7 @@@ import org.asamk.signal.manager.helper. import org.asamk.signal.manager.helper.ContactHelper; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; + import org.asamk.signal.manager.helper.IdentityHelper; import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.PreKeyHelper; @@@ -60,15 -60,10 +60,10 @@@ import org.asamk.signal.manager.storage 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; @@@ -99,9 -94,7 +94,7 @@@ import java.net.URISyntaxException import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SignatureException; - import java.util.Arrays; import java.util.Collection; - import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@@ -113,7 -106,6 +106,6 @@@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; - import java.util.function.Function; import java.util.stream.Collectors; import static org.asamk.signal.manager.config.ServiceConfig.capabilities; @@@ -139,6 -131,7 +131,7 @@@ public class ManagerImpl implements Man private final ContactHelper contactHelper; private final IncomingMessageHandler incomingMessageHandler; private final PreKeyHelper preKeyHelper; + private final IdentityHelper identityHelper; private final Context context; private boolean hasCaughtUpWithOldMessages = false; @@@ -177,14 -170,13 +170,13 @@@ this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); - final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, - account.getProfileStore()::getProfileKey, - this::getRecipientProfile, - this::getSenderCertificate); + final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account, + dependencies, + account::getProfileKey, + this::getRecipientProfile); this.profileHelper = new ProfileHelper(account, dependencies, avatarStore, - account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, this::resolveSignalServiceAddress); final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, @@@ -240,6 -232,11 +232,11 @@@ syncHelper, this::getRecipientProfile, jobExecutor); + this.identityHelper = new IdentityHelper(account, + dependencies, + this::resolveSignalServiceAddress, + syncHelper, + profileHelper); } @Override @@@ -347,19 -344,6 +344,19 @@@ syncHelper.sendConfigurationMessage(); } + @Override + public List getConfiguration() throws IOException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + final var configurationStore = account.getConfigurationStore(); + final Boolean readReceipts = configurationStore.getReadReceipts(); + final Boolean unidentifiedDeliveryIndicators = configurationStore.getUnidentifiedDeliveryIndicators(); + final Boolean typingIndicators = configurationStore.getTypingIndicators(); + final Boolean linkPreviews = configurationStore.getLinkPreviews(); + return List.of(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews); + } + /** * @param givenName if null, the previous givenName will be kept * @param familyName if null, the previous familyName will be kept @@@ -518,9 -502,12 +515,12 @@@ .map(account.getRecipientStore()::resolveRecipientAddress) .collect(Collectors.toSet()), groupInfo.isBlocked(), - groupInfo.getMessageExpirationTime(), - groupInfo.isAnnouncementGroup(), - groupInfo.isMember(account.getSelfRecipientId())); + groupInfo.getMessageExpirationTimer(), + groupInfo.getPermissionAddMember(), + groupInfo.getPermissionEditDetails(), + groupInfo.getPermissionSendMessage(), + groupInfo.isMember(account.getSelfRecipientId()), + groupInfo.isAdmin(account.getSelfRecipientId())); } @Override @@@ -545,35 -532,22 +545,22 @@@ @Override public SendGroupMessageResults updateGroup( - GroupId groupId, - String name, - String description, - Set members, - Set removeMembers, - Set admins, - Set removeAdmins, - boolean resetGroupLink, - GroupLinkState groupLinkState, - GroupPermission addMemberPermission, - GroupPermission editDetailsPermission, - File avatarFile, - Integer expirationTimer, - Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { return groupHelper.updateGroup(groupId, - name, - description, - members == null ? null : resolveRecipients(members), - removeMembers == null ? null : resolveRecipients(removeMembers), - admins == null ? null : resolveRecipients(admins), - removeAdmins == null ? null : resolveRecipients(removeAdmins), - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); + updateGroup.getName(), + updateGroup.getDescription(), + updateGroup.getMembers() == null ? null : resolveRecipients(updateGroup.getMembers()), + updateGroup.getRemoveMembers() == null ? null : resolveRecipients(updateGroup.getRemoveMembers()), + updateGroup.getAdmins() == null ? null : resolveRecipients(updateGroup.getAdmins()), + updateGroup.getRemoveAdmins() == null ? null : resolveRecipients(updateGroup.getRemoveAdmins()), + updateGroup.isResetGroupLink(), + updateGroup.getGroupLinkState(), + updateGroup.getAddMemberPermission(), + updateGroup.getEditDetailsPermission(), + updateGroup.getAvatarFile(), + updateGroup.getExpirationTimer(), + updateGroup.getIsAnnouncementGroup()); } @Override @@@ -739,7 -713,10 +726,10 @@@ @Override public void setGroupBlocked( final GroupId groupId, final boolean blocked - ) throws GroupNotFoundException, IOException { + ) throws GroupNotFoundException, IOException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } groupHelper.setGroupBlocked(groupId, blocked); // TODO cycle our profile key syncHelper.sendBlockedList(); @@@ -806,22 -783,6 +796,6 @@@ } } - 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()) { @@@ -1080,7 -1041,7 +1054,7 @@@ return toGroup(groupHelper.getGroup(groupId)); } - public GroupInfo getGroupInfo(GroupId groupId) { + private GroupInfo getGroupInfo(GroupId groupId) { return groupHelper.getGroup(groupId); } @@@ -1101,8 -1062,9 +1075,9 @@@ final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); return new Identity(address, identityInfo.getIdentityKey(), - computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), - computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), + identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), + identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), + identityInfo.getIdentityKey()).getSerialized(), identityInfo.getTrustLevel(), identityInfo.getDateAdded()); } @@@ -1132,9 -1094,7 +1107,7 @@@ } catch (UnregisteredUserException e) { return false; } - return trustIdentity(recipientId, - identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), - TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerified(recipientId, fingerprint); } /** @@@ -1151,10 -1111,7 +1124,7 @@@ } catch (UnregisteredUserException e) { return false; } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, - identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), - TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); } /** @@@ -1171,15 -1128,7 +1141,7 @@@ } catch (UnregisteredUserException e) { return false; } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, identityKey -> { - final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); - try { - return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber); - } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { - return false; - } - }, TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); } /** @@@ -1195,66 -1144,13 +1157,13 @@@ } catch (UnregisteredUserException e) { return false; } - return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); - } - - private boolean trustIdentity( - RecipientId recipientId, Function verifier, TrustLevel trustLevel - ) { - var identity = account.getIdentityKeyStore().getIdentity(recipientId); - if (identity == null) { - return false; - } - - if (!verifier.apply(identity.getIdentityKey())) { - return false; - } - - account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); - try { - var address = resolveSignalServiceAddress(recipientId); - syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); - } catch (IOException e) { - logger.warn("Failed to send verification sync message: {}", e.getMessage()); - } - - return true; + return identityHelper.trustIdentityAllKeys(recipientId); } private void handleIdentityFailure( final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure ) { - final var identityKey = identityFailure.getIdentityKey(); - if (identityKey != null) { - final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } else { - // Retrieve profile to get the current identity key from the server - profileHelper.refreshRecipientProfile(recipientId); - } - } - - @Override - public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); - } - - private byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); - } - - private Fingerprint computeSafetyNumberFingerprint( - final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey - ) { - return Utils.computeSafetyNumber(capabilities.isUuid(), - account.getSelfAddress(), - account.getIdentityKeyPair().getPublicKey(), - theirAddress, - theirIdentityKey); + this.identityHelper.handleIdentityFailure(recipientId, identityFailure); } @Override @@@ -1329,5 -1225,4 +1238,4 @@@ } account = null; } - } diff --combined man/signal-cli-dbus.5.adoc index 508a7881,55058580..1a0fbfaa --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@@ -29,7 -29,7 +29,7 @@@ method(arg1, arg2, ...) -> Where is according to DBus specification: - * : Array of ... (comma-separated list, array:) + * : Array of ... (comma-separated list) (array:) * (...) : Struct (cannot be sent via `dbus-send`) * : Boolean (false|true) (boolean:) * : Signed 32-bit (int) integer (int32:) @@@ -48,9 -48,9 +48,9 @@@ Phone numbers always have the format +< == Methods === Control methods - These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`). - Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to - `/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_). + These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`). + Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to + `/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_). Only `version()` is activated in single-user mode; the rest are disabled. link() -> deviceLinkUri:: @@@ -58,12 -58,12 +58,12 @@@ link(newDeviceName) -> deviceLinkUri * newDeviceName : Name to give new device (defaults to "cli" if no name is given) * deviceLinkUri : URI of newly linked device - Returns a URI of the form "tsdevice:/?uuid=...". This can be piped to a QR encoder to create a display that + Returns a URI of the form "sgnl://linkdevice/?uuid=...". This can be piped to a QR encoder to create a display that can be captured by a Signal smartphone client. For example: `dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client"|tr '\n' '\0'|sed 's/.*string //g'|sed 's/\"//g'|qrencode -s10 -tANSI256` - Exception: Failure + Exceptions: Failure listAccounts() -> accountList:: * accountList : Array of all attached accounts in DBus object path form @@@ -89,94 -89,243 +89,243 @@@ verify(number, verificationCode) Command fails if PIN was set after previous registration; use verifyWithPin instead. - Exception: Failure, InvalidNumber + Exceptions: Failure, InvalidNumber verifyWithPin(number, verificationCode, pin) -> <>:: * number : Phone number * verificationCode : Code received from Signal after successful registration request * pin : PIN you set with setPin command after verifying previous registration - Exception: Failure, InvalidNumber + Exceptions: Failure, InvalidNumber version() -> version:: * version : Version string of signal-cli Exceptions: None - === Other methods + === Group control methods + The following methods listen to the recipient's object path, which is constructed as follows: + "/org/asamk/Signal/" + DBusNumber + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) - updateGroup(groupId, newName, members, avatar) -> groupId:: - * groupId : Byte array representing the internal group identifier - * newName : New name of group (empty if unchanged) - * members : String array of new members to be invited to group - * avatar : Filename of avatar picture to be set for group (empty if none) + createGroup(groupName, members, avatar) -> groupId:: + * groupName : String representing the display name of the group + * members : String array of new members to be invited to group + * avatar : Filename of avatar picture to be set for group (empty if none) + * groupId : Byte array representing the internal group identifier - Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound + Exceptions: AttachmentInvalid, Failure, InvalidNumber; - updateProfile(name, about, aboutEmoji , avatar, remove) -> <>:: - updateProfile(givenName, familyName, about, aboutEmoji , avatar, remove) -> <>:: - * name : Name for your own profile (empty if unchanged) - * givenName : Given name for your own profile (empty if unchanged) - * familyName : Family name for your own profile (empty if unchanged) - * about : About message for profile (empty if unchanged) - * aboutEmoji : Emoji for profile (empty if unchanged) - * avatar : Filename of avatar picture for profile (empty if unchanged) - * remove : Set to true if the existing avatar picture should be removed + getGroup(groupId) -> objectPath:: + * groupId : Byte array representing the internal group identifier + * objectPath : DBusPath for the group + + getGroupMembers(groupId) -> members:: + * groupId : Byte array representing the internal group identifier + * members : String array with the phone numbers of all active members of a group + + Exceptions: None, if the group name is not found an empty array is returned + + joinGroup(inviteURI) -> <>:: + * inviteURI : String starting with https://signal.group/# + + Behavior of this method depends on the `requirePermission` parameter of the `enableLink` method. If permission is required, `joinGroup` adds you to the requesting members list. Permission may be granted based on the group's `PermissionAddMember` property (`ONLY_ADMINS` or `EVERY_MEMBER`). If permission is not required, `joinGroup` admits you immediately to the group. Exceptions: Failure + listGroups() -> groups:: + * groups : Array of Structs(objectPath, groupId, groupName) + ** objectPath : DBusPath + ** groupId : Byte array representing the internal group identifier + ** groupName : String representing the display name of the group - 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. + sendGroupMessage(message, attachments, groupId) -> timestamp:: + * message : Text to send (can be UTF8) + * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) + * groupId : Byte array representing the internal group identifier + * timestamp : Long, can be used to identify the corresponding Signal reply + + Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId + + sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, groupId) -> timestamp:: + * emoji : Unicode grapheme cluster of the emoji + * remove : Boolean, whether a previously sent reaction (emoji) should be removed + * targetAuthor : String with the phone number of the author of the message to which to react + * targetSentTimestamp : Long representing timestamp of the message to which to react + * groupId : Byte array representing the internal group identifier + * timestamp : Long, can be used to identify the corresponding signal reply + + Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId + + sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId) -> timestamp:: + * targetSentTimestamp : Long representing timestamp of the message to delete + * groupId : Byte array with base64 encoded group identifier + * timestamp : Long, can be used to identify the corresponding signal reply + + Exceptions: Failure, GroupNotFound, InvalidGroupId + + === Group methods + The following methods listen to the group's object path, which can be obtained from the listGroups() method and is constructed as follows: + "/org/asamk/Signal/" + DBusNumber + "/Groups/" + DBusGroupId + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) + * DBusGroupId : groupId in base64 format, with underscore (_) replacing plus (+), equals (=), or slash (/) + + Groups have the following (case-sensitive) properties: + * Id (read-only) : Byte array representing the internal group identifier + * Name : Display name of the group + * Description : Description of the group + * Avatar (write-only) : Filename of the avatar + * IsBlocked : true=member will not receive group messages; false=not blocked + * IsMember (read-only) : always true (object path exists only for group members) + * IsAdmin (read-only) : true=member has admin privileges; false=not admin + * MessageExpirationTimer : int32 representing message expiration time for group + * Members (read-only) : String array of group members' phone numbers + * PendingMembers (read-only) : String array of pending members' phone numbers + * RequestingMembers (read-only) : String array of requesting members' phone numbers + * Admins (read-only) : String array of admins' phone numbers + * PermissionAddMember : String representing who has permission to add members + ** ONLY_ADMINS, EVERY_MEMBER + * PermissionEditDetails : String representing who may edit group details + ** ONLY_ADMINS, EVERY_MEMBER + * PermissionSendMessage : String representing who post messages to group + ** ONLY_ADMINS, EVERY_MEMBER (note that ONLY_ADMINS is equivalent to IsAnnouncementGroup) + * GroupInviteLink (read-only) : String of the invitation link (starts with https://signal.group/#) + + To get a property, use (replacing `--session` with `--system` if needed): + `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Group string:$PROPERTY_NAME` + + To set a property, use: + `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Group string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE` + + To get all properties, use: + `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Group` + + addAdmins(recipients) -> <>:: + * recipients : String array of phone numbers + + Grant admin privileges to recipients. Exceptions: Failure - setContactBlocked(number, block) -> <>:: - * number : Phone number affected by method - * block : 0=remove block , 1=blocked + addMembers(recipients) -> <>:: + * recipients : String array of phone numbers - Messages from blocked numbers will no longer be forwarded via DBus. + Add recipients to group if they are pending members; otherwise add recipients to list of requesting members. - Exceptions: InvalidNumber + Exceptions: Failure - setGroupBlocked(groupId, block) -> <>:: - * groupId : Byte array representing the internal group identifier - * block : 0=remove block , 1=blocked + disableLink() -> <>:: - Messages from blocked groups will no longer be forwarded via DBus. + Disables the group's invitation link. + + Exceptions: Failure - Exceptions: GroupNotFound + enableLink(requiresApproval) -> <>:: + * requiresApproval : true=add numbers using the link to the requesting members list - joinGroup(inviteURI) -> <>:: - * inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App + Enables the group's invitation link. + + Exceptions: Failure + + quitGroup() -> <>:: + Exceptions: Failure, LastGroupAdmin + + removeAdmins(recipients) -> <>:: + * recipients : String array of phone numbers + + Remove admin privileges from recipients. Exceptions: Failure + removeMembers(recipients) -> <>:: + * recipients : String array of phone numbers + + Remove recipients from group. + + Exceptions: Failure + + resetLink() -> <>:: + + Resets the group's invitation link to a new random URL starting with https://signal.group/# + + Exceptions: Failure + + === Deprecated group control methods + The following deprecated methods listen to the recipient's object path, which is constructed as follows: + "/org/asamk/Signal/" + DBusNumber + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) + + getGroupIds() -> groupList:: + groupList : Array of Byte arrays representing the internal group identifiers + + All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() + + Exceptions: None + + getGroupName(groupId) -> groupName:: + * groupId : Byte array representing the internal group identifier + * groupName : The display name of the group + + Exceptions: None, if the group name is not found an empty string is returned + + isGroupBlocked(groupId) -> isGroupBlocked:: + * groupId : Byte array representing the internal group identifier + * isGroupBlocked : true=group is blocked; false=group is not blocked + + Dbus will not forward messages from a group when you have blocked it. + + Exceptions: InvalidGroupId, Failure + + isMember(groupId) -> isMember:: + * groupId : Byte array representing the internal group identifier + * isMember : true=you are a group member; false=you are not a group member + + Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) + quitGroup(groupId) -> <>:: * groupId : Byte array representing the internal group identifier Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember() - Exceptions: GroupNotFound, Failure + Exceptions: GroupNotFound, Failure, InvalidGroupId - isMember(groupId) -> active:: + setGroupBlocked(groupId, block) -> <>:: * groupId : Byte array representing the internal group identifier + * block : false=remove block , true=blocked - Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) + Messages from blocked groups will no longer be forwarded via DBus. - sendEndSessionMessage(recipients) -> <>:: - * recipients : Array of phone numbers + Exceptions: GroupNotFound, InvalidGroupId - Exceptions: Failure, InvalidNumber, UntrustedIdentity + updateGroup(groupId, newName, members, avatar) -> groupId:: + * groupId : Byte array representing the internal group identifier + * newName : New name of group (empty if unchanged) + * members : String array of new members to be invited to group + * avatar : Filename of avatar picture to be set for group (empty if none) - sendGroupMessage(message, attachments, groupId) -> timestamp:: - * message : Text to send (can be UTF8) - * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) - * groupId : Byte array representing the internal group identifier - * timestamp : Can be used to identify the corresponding signal reply + Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound + + === Device control methods + The following methods listen to the recipient's object path, which is constructed as follows: + "/org/asamk/Signal/" + DBusNumber + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) + + addDevice(deviceUri) -> <>:: + * deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. + + getDevice(deviceId) -> devicePath:: + * deviceId : Long representing a deviceId + * devicePath : DBusPath object for the device + + Exceptions: DeviceNotFound + + listDevices() -> devices:: + * devices : Array of structs (objectPath, id, name) + ** objectPath : DBusPath representing the device's object path + ** id : Long representing the deviceId + ** name : String representing the device's name - Exceptions: GroupNotFound, Failure, AttachmentInvalid + Exceptions: InvalidUri sendContacts() -> <>:: @@@ -188,49 -337,103 +337,103 @@@ sendSyncRequest() -> <>: Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device. - Exception: Failure + Exceptions: Failure - sendNoteToSelfMessage(message, attachments) -> timestamp:: - * message : Text to send (can be UTF8) - * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) - * timestamp : Can be used to identify the corresponding signal reply + === Device methods and properties + The following methods listen to the device's object path, which is constructed as follows: + "/org/asamk/Signal/" + DBusNumber + "/Devices/" + deviceId + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) + * deviceId : Long representing the device identifier (obtained from listDevices() method) - Exceptions: Failure, AttachmentInvalid + Devices have the following (case-sensitive) properties: + * Id (read-only) : Long representing the device identifier + * Created (read-only) : Long representing the number of milliseconds since the Unix epoch + * LastSeen (read-only) : Long representing the number of milliseconds since the Unix epoch + * Name : String representing the display name of the device - sendMessage(message, attachments, recipient) -> timestamp:: - sendMessage(message, attachments, recipients) -> timestamp:: - * message : Text to send (can be UTF8) - * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) - * recipient : Phone number of a single recipient - * recipients : Array of phone numbers - * timestamp : Can be used to identify the corresponding signal reply + To get a property, use (replacing `--session` with `--system` if needed): + `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Device string:$PROPERTY_NAME` - Depending on the type of the recipient field this sends a message to one or multiple recipients. + To set a property, use: + `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Device string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE` - Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity + To get all properties, use: + `dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Device` - sendTyping(recipient, stop) -> <>:: - * recipient : Phone number of a single recipient - * targetSentTimestamp : True, if typing state should be stopped + removeDevice() -> <>:: - Exceptions: Failure, GroupNotFound, UntrustedIdentity + Exceptions: Failure + === Other methods - sendReadReceipt(recipient, targetSentTimestamp) -> <>:: - * recipient : Phone number of a single recipient - * targetSentTimestamp : Array of Longs to identify the corresponding signal messages + getContactName(number) -> name:: + * number : Phone number + * name : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used - Exceptions: Failure, UntrustedIdentity + Exceptions: None - sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, groupId) -> timestamp:: - * emoji : Unicode grapheme cluster of the emoji - * remove : Boolean, whether a previously sent reaction (emoji) should be removed - * targetAuthor : String with the phone number of the author of the message to which to react - * targetSentTimestamp : Long representing timestamp of the message to which to react - * groupId : Byte array with base64 encoded group identifier - * timestamp : Long, can be used to identify the corresponding signal reply + getContactNumber(name) -> numbers:: + * numbers : Array of phone number + * name : Contact or profile name ("firstname lastname") + + Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. + + Exceptions: None + + getSelfNumber() -> number:: + * number : Your phone number + + Exceptions: None + + isContactBlocked(number) -> blocked:: + * number : Phone number + * blocked : true=blocked, false=not blocked + + For unknown numbers false is returned but no exception is raised. + + Exceptions: InvalidPhoneNumber + + isRegistered() -> result:: + isRegistered(number) -> result:: + isRegistered(numbers) -> results:: + * number : Phone number + * numbers : String array of phone numbers + * result : true=number is registered, false=number is not registered + * results : Boolean array of results + + For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered). + + Exceptions: InvalidNumber + + listNumbers() -> numbers:: + * numbers : String array of all known numbers + + This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) + + Exceptions: None + + removePin() -> <>:: + + Removes registration PIN protection. + + Exceptions: Failure + + sendEndSessionMessage(recipients) -> <>:: + * recipients : Array of phone numbers + + Exceptions: Failure, InvalidNumber, UntrustedIdentity + + sendMessage(message, attachments, recipient) -> timestamp:: + sendMessage(message, attachments, recipients) -> timestamp:: + * message : Text to send (can be UTF8) + * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) + * recipient : Phone number of a single recipient + * recipients : String array of phone numbers + * timestamp : Long, can be used to identify the corresponding Signal reply - Exceptions: Failure, InvalidNumber, GroupNotFound + Depending on the type of the recipient field this sends a message to one or multiple recipients. + + Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipient) -> timestamp:: sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients) -> timestamp:: @@@ -240,18 -443,24 +443,24 @@@ * targetSentTimestamp : Long representing timestamp of the message to which to react * recipient : String with the phone number of a single recipient * recipients : Array of strings with phone numbers, should there be more recipients - * timestamp : Long, can be used to identify the corresponding signal reply + * timestamp : Long, can be used to identify the corresponding Signal reply Depending on the type of the recipient(s) field this sends a reaction to one or multiple recipients. Exceptions: Failure, InvalidNumber - sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId) -> timestamp:: - * targetSentTimestamp : Long representing timestamp of the message to delete - * groupId : Byte array with base64 encoded group identifier - * timestamp : Long, can be used to identify the corresponding signal reply + sendNoteToSelfMessage(message, attachments) -> timestamp:: + * message : Text to send (can be UTF8) + * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) + * timestamp : Long, can be used to identify the corresponding Signal reply - Exceptions: Failure, GroupNotFound + Exceptions: Failure, AttachmentInvalid + + sendReadReceipt(recipient, targetSentTimestamps) -> <>:: + * recipient : Phone number of a single recipient + * targetSentTimestamps : Array of Longs to identify the corresponding Signal messages + + Exceptions: Failure, UntrustedIdentity sendRemoteDeleteMessage(targetSentTimestamp, recipient) -> timestamp:: sendRemoteDeleteMessage(targetSentTimestamp, recipients) -> timestamp:: @@@ -264,140 -473,78 +473,98 @@@ Depending on the type of the recipient( Exceptions: Failure, InvalidNumber - getContactName(number) -> name:: - * number : Phone number - * name : Contact's name in local storage (from the primary device for a linked account, or the one set with setContactName); if not set, contact's profile name is used - - setContactName(number,name<>) -> <>:: - * number : Phone number - * name : Name to be set in contacts (in local storage with signal-cli) - - getGroupIds() -> groupList:: - groupList : Array of Byte arrays representing the internal group identifiers - - All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() - - getGroupName(groupId) -> groupName:: - groupName : The display name of the group - groupId : Byte array representing the internal group identifier - - Exceptions: None, if the group name is not found an empty string is returned - - getGroupMembers(groupId) -> members:: - members : String array with the phone numbers of all active members of a group - groupId : Byte array representing the internal group identifier - - Exceptions: None, if the group name is not found an empty array is returned + sendTyping(recipient, stop) -> <>:: + * recipient : Phone number of a single recipient + * targetSentTimestamp : True, if typing state should be stopped - listNumbers() -> numbers:: - numbers : String array of all known numbers + Exceptions: Failure, GroupNotFound, UntrustedIdentity - This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) + setContactBlocked(number, block) -> <>:: + * number : Phone number affected by method + * block : false=remove block, true=blocked - getContactNumber(name) -> numbers:: - * numbers : Array of phone number - * name : Contact or profile name ("firstname lastname") + Messages from blocked numbers will no longer be forwarded via DBus. - Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. + Exceptions: InvalidNumber - isContactBlocked(number) -> state:: + setContactName(number,name<>) -> <>:: * number : Phone number - * state : 1=blocked, 0=not blocked - - Exceptions: None, for unknown numbers 0 (false) is returned - - isGroupBlocked(groupId) -> state:: - * groupId : Byte array representing the internal group identifier - * state : 1=blocked, 0=not blocked - - Exceptions: None, for unknown groups 0 (false) is returned + * name : Name to be set in contacts (in local storage with signal-cli) - removePin() -> <>:: + Exceptions: InvalidNumber, Failure - Removes registration PIN protection. + 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. - Exception: Failure + Exceptions: Failure, InvalidNumber 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 - - 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 - - Exception: InvalidUri - - listDevices() -> devices:: - * devices : Array of structs (objectPath, id, name) - ** objectPath : DBusPath representing the device's object path - ** id : Long representing the deviceId - ** name : String representing the device's name - - Exception: Failure - - removeDevice(deviceId) -> <>:: - * deviceId : Device ID to remove, obtained from listDevices() command + Exceptions: Failure - Exception: Failure + submitRateLimitChallenge(challenge, captcha) -> <>:: + * challenge : The challenge token taken from the proof required error. + * captcha : The captcha token from the solved captcha on the Signal website.. + Can be used to lift some rate-limits by solving a captcha. - updateDeviceName(deviceName) -> <>:: - * deviceName : New name + Exception: IOErrorException - Set a new name for this device (main or linked). + updateProfile(name, about, aboutEmoji , avatar, remove) -> <>:: + updateProfile(givenName, familyName, about, aboutEmoji , avatar, remove) -> <>:: + * name : Name for your own profile (empty if unchanged) + * givenName : Given name for your own profile (empty if unchanged) + * familyName : Family name for your own profile (empty if unchanged) + * about : About message for profile (empty if unchanged) + * aboutEmoji : Emoji for profile (empty if unchanged) + * avatar : Filename of avatar picture for profile (empty if unchanged) + * remove : Set to true if the existing avatar picture should be removed - Exception: Failure + Exceptions: Failure uploadStickerPack(stickerPackPath) -> url:: * stickerPackPath : Path to the manifest.json file or a zip file in the same directory * url : URL of sticker pack after successful upload -Exceptions: Failure +Exception: Failure, IOError + +getConfiguration() -> [readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews] -> <>:: +* readReceipts : Should Signal send read receipts (true/false). +* unidentifiedDeliveryIndicators : Should Signal show unidentified delivery indicators (true/false). +* typingIndicators : Should Signal send/show typing indicators (true/false). +* linkPreviews : Should Signal generate link previews (true/false). + +Gets an array of four booleans as indicated. Only works from primary device. + +Exceptions: IOError, UserError + +setConfiguration(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews) -> <>:: +* readReceipts : Should Signal send read receipts (true/false). +* unidentifiedDeliveryIndicators : Should Signal show unidentified delivery indicators (true/false). +* typingIndicators : Should Signal send/show typing indicators (true/false). +* linkPreviews : Should Signal generate link previews (true/false). + +Update Signal configurations and sync them to linked devices. Only works from primary device. + +Exceptions: IOError, UserError - submitRateLimitChallenge(challenge, captcha) -> <>:: - * challenge : The challenge token taken from the proof required error. - * captcha : The captcha token from the solved captcha on the Signal website.. - Can be used to lift some rate-limits by solving a captcha. + version() -> version:: + * version : Version string of signal-cli - Exception: IOErrorException + Exceptions: None == Signals - SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: + SyncMessageReceived (timestamp, sender, destination, groupId, message, attachments):: + * timestamp : Integer value that can be used to associate this e.g. with a sendMessage() + * sender : Phone number of the sender + * destination : DBus code for destination + * groupId : Byte array representing the internal group identifier (empty when private message) + * message : Message text + * attachments : String array of filenames in the signal-cli storage (~/.local/share/signal-cli/attachments/) + The sync message is received when the user sends a message from a linked device. ReceiptReceived (timestamp, sender):: @@@ -411,7 -558,7 +578,7 @@@ MessageReceived(timestamp, sender * sender : Phone number of the sender * groupId : Byte array representing the internal group identifier (empty when private message) * message : Message text - * attachments : String array of filenames for the attachments. These files are located in the signal-cli storage and the current user needs to have read access there + * attachments : String array of filenames in the signal-cli storage (~/.local/share/signal-cli/attachments/) This signal is received whenever we get a private message or a message is posted in a group we are an active member diff --combined src/main/java/org/asamk/Signal.java index 865db815,349671b3..28c192a0 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@@ -1,7 -1,6 +1,6 @@@ package org.asamk; import org.asamk.signal.commands.exceptions.IOErrorException; - import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.annotations.DBusProperty; @@@ -84,14 -83,27 +83,27 @@@ public interface Signal extends DBusInt void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; + @Deprecated void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId; + @Deprecated List getGroupIds(); + DBusPath getGroup(byte[] groupId); + + List listGroups(); + + @Deprecated String getGroupName(byte[] groupId) throws Error.InvalidGroupId; + @Deprecated List getGroupMembers(byte[] groupId) throws Error.InvalidGroupId; + byte[] createGroup( + String name, List members, String avatar + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber; + + @Deprecated byte[] updateGroup( byte[] groupId, String name, List members, String avatar ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId; @@@ -133,22 -145,21 +145,25 @@@ List getContactNumber(final String name) throws Error.Failure; + @Deprecated void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId; boolean isContactBlocked(final String number) throws Error.InvalidNumber; + @Deprecated boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId; + @Deprecated boolean isMember(final byte[] groupId) throws Error.InvalidGroupId; byte[] joinGroup(final String groupLink) throws Error.Failure; String uploadStickerPack(String stickerPackPath) throws Error.Failure; + void setConfiguration(boolean readReceipts, boolean unidentifiedDeliveryIndicators, boolean typingIndicators, boolean linkPreviews) throws Error.IOError, Error.UserError; + + List getConfiguration(); + void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException; class MessageReceived extends DBusSignal { @@@ -307,6 -318,71 +322,71 @@@ void removeDevice() throws Error.Failure; } + class StructGroup extends Struct { + + @Position(0) + DBusPath objectPath; + + @Position(1) + byte[] id; + + @Position(2) + String name; + + public StructGroup(final DBusPath objectPath, final byte[] id, final String name) { + this.objectPath = objectPath; + this.id = id; + this.name = name; + } + + public DBusPath getObjectPath() { + return objectPath; + } + + public byte[] getId() { + return id; + } + + public String getName() { + return name; + } + } + + @DBusProperty(name = "Id", type = Byte[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Name", type = String.class) + @DBusProperty(name = "Description", type = String.class) + @DBusProperty(name = "Avatar", type = String.class, access = DBusProperty.Access.WRITE) + @DBusProperty(name = "IsBlocked", type = Boolean.class) + @DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "MessageExpirationTimer", type = Integer.class) + @DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "RequestingMembers", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Admins", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "PermissionAddMember", type = String.class) + @DBusProperty(name = "PermissionEditDetails", type = String.class) + @DBusProperty(name = "PermissionSendMessage", type = String.class) + @DBusProperty(name = "GroupInviteLink", type = String.class, access = DBusProperty.Access.READ) + interface Group extends DBusInterface, Properties { + + void quitGroup() throws Error.Failure, Error.LastGroupAdmin; + + void addMembers(List recipients) throws Error.Failure; + + void removeMembers(List recipients) throws Error.Failure; + + void addAdmins(List recipients) throws Error.Failure; + + void removeAdmins(List recipients) throws Error.Failure; + + void resetLink() throws Error.Failure; + + void disableLink() throws Error.Failure; + + void enableLink(boolean requiresApproval) throws Error.Failure; + } + interface Error { class AttachmentInvalid extends DBusExecutionException { @@@ -330,6 -406,13 +410,13 @@@ } } + class DeviceNotFound extends DBusExecutionException { + + public DeviceNotFound(final String message) { + super(message); + } + } + class GroupNotFound extends DBusExecutionException { public GroupNotFound(final String message) { @@@ -344,6 -427,13 +431,13 @@@ } } + class LastGroupAdmin extends DBusExecutionException { + + public LastGroupAdmin(final String message) { + super(message); + } + } + class InvalidNumber extends DBusExecutionException { public InvalidNumber(final String message) { @@@ -357,19 -447,5 +451,19 @@@ super(message); } } + + class IOError extends DBusExecutionException { + + public IOError(final String message) { + super(message); + } + } + + class UserError extends DBusExecutionException { + + public UserError(final String message) { + super(message); + } + } } } diff --combined src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 226402dd,59422e69..a8f071f7 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@@ -15,9 -15,9 +15,9 @@@ import org.asamk.signal.manager.api.Rec import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; + import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; - import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; @@@ -30,7 -30,6 +30,6 @@@ import org.freedesktop.dbus.DBusPath import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBusInterface; - import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@@ -108,17 -107,7 +107,17 @@@ public class DbusManagerImpl implement final Boolean typingIndicators, final Boolean linkPreviews ) throws IOException { - throw new UnsupportedOperationException(); + signal.setConfiguration( + readReceipts, + unidentifiedDeliveryIndicators, + typingIndicators, + linkPreviews + ); + } + + @Override + public List getConfiguration() { + return signal.getConfiguration(); } @Override @@@ -193,8 -182,8 +192,8 @@@ @Override public List getGroups() { - final var groupIds = signal.getGroupIds(); - return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList()); + final var groups = signal.listGroups(); + return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList()); } @Override @@@ -204,7 -193,8 +203,8 @@@ if (groupAdmins.size() > 0) { throw new UnsupportedOperationException(); } - signal.quitGroup(groupId.serialize()); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.quitGroup(); return new SendGroupMessageResults(0, List.of()); } @@@ -217,8 -207,7 +217,7 @@@ public Pair createGroup( final String name, final Set members, final File avatarFile ) throws IOException, AttachmentInvalidException { - final var newGroupId = signal.updateGroup(new byte[0], - emptyIfNull(name), + final var newGroupId = signal.createGroup(emptyIfNull(name), members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), avatarFile == null ? "" : avatarFile.getPath()); return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); @@@ -226,25 -215,76 +225,76 @@@ @Override public SendGroupMessageResults updateGroup( - final GroupId groupId, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { - signal.updateGroup(groupId.serialize(), - emptyIfNull(name), - members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), - avatarFile == null ? "" : avatarFile.getPath()); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + if (updateGroup.getName() != null) { + group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName()); + } + if (updateGroup.getDescription() != null) { + group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription()); + } + if (updateGroup.getAvatarFile() != null) { + group.Set("org.asamk.Signal.Group", + "Avatar", + updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath()); + } + if (updateGroup.getExpirationTimer() != null) { + group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer()); + } + if (updateGroup.getAddMemberPermission() != null) { + group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name()); + } + if (updateGroup.getEditDetailsPermission() != null) { + group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name()); + } + if (updateGroup.getIsAnnouncementGroup() != null) { + group.Set("org.asamk.Signal.Group", + "PermissionSendMessage", + updateGroup.getIsAnnouncementGroup() + ? GroupPermission.ONLY_ADMINS.name() + : GroupPermission.EVERY_MEMBER.name()); + } + if (updateGroup.getMembers() != null) { + group.addMembers(updateGroup.getMembers() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getRemoveMembers() != null) { + group.removeMembers(updateGroup.getRemoveMembers() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getAdmins() != null) { + group.addAdmins(updateGroup.getAdmins() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getRemoveAdmins() != null) { + group.removeAdmins(updateGroup.getRemoveAdmins() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.isResetGroupLink()) { + group.resetLink(); + } + if (updateGroup.getGroupLinkState() != null) { + switch (updateGroup.getGroupLinkState()) { + case DISABLED: + group.disableLink(); + break; + case ENABLED: + group.enableLink(false); + break; + case ENABLED_WITH_APPROVAL: + group.enableLink(true); + break; + } + } return new SendGroupMessageResults(0, List.of()); } @@@ -354,7 -394,12 +404,12 @@@ public void setGroupBlocked( final GroupId groupId, final boolean blocked ) throws GroupNotFoundException, IOException { - signal.setGroupBlocked(groupId.serialize(), blocked); + setGroupProperty(groupId, "IsBlocked", blocked); + } + + private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) { + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.Set("org.asamk.Signal.Group", propertyName, blocked); } @Override @@@ -421,19 -466,41 +476,41 @@@ @Override public Group getGroup(final GroupId groupId) { - final var id = groupId.serialize(); - return new Group(groupId, - signal.getGroupName(id), - null, - null, - signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()), - Set.of(), - Set.of(), - Set.of(), - signal.isGroupBlocked(id), - 0, - false, - signal.isMember(id)); + final var groupPath = signal.getGroup(groupId.serialize()); + return getGroup(groupPath); + } + + @SuppressWarnings("unchecked") + private Group getGroup(final DBusPath groupPath) { + final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group"); + final var id = (byte[]) group.get("Id").getValue(); + try { + return new Group(GroupId.unknownVersion(id), + (String) group.get("Name").getValue(), + (String) group.get("Description").getValue(), + GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()), + ((List) group.get("Members").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("PendingMembers").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("RequestingMembers").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("Admins").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + (boolean) group.get("IsBlocked").getValue(), + (int) group.get("MessageExpirationTimer").getValue(), + GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()), + GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()), + GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()), + (boolean) group.get("IsMember").getValue(), + (boolean) group.get("IsAdmin").getValue()); + } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { + throw new AssertionError(e); + } } @Override @@@ -470,13 -537,6 +547,6 @@@ 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; diff --combined src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 41eca5da,2cf3c813..c6e3273a --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@@ -1,7 -1,6 +1,6 @@@ package org.asamk.signal.dbus; import org.asamk.Signal; - import org.asamk.Signal.Error; import org.asamk.signal.BaseConfig; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.AttachmentInvalidException; @@@ -13,9 -12,12 +12,12 @@@ import org.asamk.signal.manager.api.Ide import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; + import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; + import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; + import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@@ -26,6 -28,7 +28,7 @@@ import org.freedesktop.dbus.DBusPath import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; + import org.freedesktop.dbus.types.Variant; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@@ -40,6 -43,8 +43,8 @@@ import java.io.IOException import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; + import java.util.Arrays; + import java.util.Base64; import java.util.Collection; import java.util.HashSet; import java.util.List; @@@ -58,6 -63,7 +63,7 @@@ public class DbusSignalImpl implements private DBusPath thisDevice; private final List devices = new ArrayList<>(); + private final List groups = new ArrayList<>(); public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) { this.m = m; @@@ -67,6 -73,7 +73,7 @@@ public void initObjects() { updateDevices(); + updateGroups(); } public void close() { @@@ -111,7 -118,11 +118,11 @@@ @Override public DBusPath getDevice(long deviceId) { updateDevices(); - return new DBusPath(getDeviceObjectPath(objectPath, deviceId)); + final var deviceOptional = devices.stream().filter(g -> g.getId().equals(deviceId)).findFirst(); + if (deviceOptional.isEmpty()) { + throw new Error.DeviceNotFound("Device not found"); + } + return deviceOptional.get().getObjectPath(); } @Override @@@ -394,6 -405,8 +405,8 @@@ public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { m.setGroupBlocked(getGroupId(groupId), blocked); + } catch (NotMasterDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException e) { @@@ -411,6 -424,22 +424,22 @@@ return ids; } + @Override + public DBusPath getGroup(final byte[] groupId) { + updateGroups(); + final var groupOptional = groups.stream().filter(g -> Arrays.equals(g.getId(), groupId)).findFirst(); + if (groupOptional.isEmpty()) { + throw new Error.GroupNotFound("Group not found"); + } + return groupOptional.get().getObjectPath(); + } + + @Override + public List listGroups() { + updateGroups(); + return groups; + } + @Override public String getGroupName(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); @@@ -427,10 -456,18 +456,18 @@@ if (group == null) { return List.of(); } else { - return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); + final var members = group.getMembers(); + return getRecipientStrings(members); } } + @Override + public byte[] createGroup( + final String name, final List members, final String avatar + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber { + return updateGroup(new byte[0], name, members, avatar); + } + @Override public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) { try { @@@ -444,19 -481,11 +481,11 @@@ return results.first().serialize(); } else { final var results = m.updateGroup(getGroupId(groupId), - name, - null, - memberIdentifiers, - null, - null, - null, - false, - null, - null, - null, - avatar == null ? null : new File(avatar), - null, - null); + UpdateGroup.newBuilder() + .withName(name) + .withMembers(memberIdentifiers) + .withAvatarFile(avatar == null ? null : new File(avatar)) + .build()); if (results != null) { checkSendMessageResults(results.getTimestamp(), results.getResults()); } @@@ -667,36 -696,12 +696,36 @@@ try { return m.uploadStickerPack(path).toString(); } catch (IOException e) { - throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage()); + throw new Error.IOError("Upload error (maybe image size is too large):" + e.getMessage()); } catch (StickerPackInvalidException e) { throw new Error.Failure("Invalid sticker pack: " + e.getMessage()); } } + @Override + public void setConfiguration(boolean readReceipts, boolean unidentifiedDeliveryIndicators, boolean typingIndicators, boolean linkPreviews) { + try { + m.updateConfiguration(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews); + } catch (IOException e) { + throw new Error.IOError("UpdateAccount error: " + e.getMessage()); + } catch (NotMasterDeviceException e) { + throw new Error.UserError("This command doesn't work on linked devices."); + } + } + + @Override + public List getConfiguration() { + List config = new ArrayList<>(4); + try { + config = m.getConfiguration(); + } catch (IOException e) { + throw new Error.IOError("Configuration storage error: " + e.getMessage()); + } catch (NotMasterDeviceException e) { + throw new Error.UserError("This command doesn't work on linked devices."); + } + return config; + } + private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { var error = ErrorUtils.getErrorMessageFromSendMessageResult(result); @@@ -760,6 -765,10 +789,10 @@@ throw new Error.Failure(message.toString()); } + private static List getRecipientStrings(final Set members) { + return members.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); + } + private static Set getSingleRecipientIdentifiers( final Collection recipientStrings, final String localNumber ) throws DBusExecutionException { @@@ -837,6 -846,38 +870,38 @@@ this.devices.clear(); } + private static String getGroupObjectPath(String basePath, byte[] groupId) { + return basePath + "/Groups/" + Base64.getEncoder() + .encodeToString(groupId) + .replace("+", "_") + .replace("/", "_") + .replace("=", "_"); + } + + private void updateGroups() { + List groups; + groups = m.getGroups(); + + unExportGroups(); + + groups.forEach(g -> { + final var object = new DbusSignalGroupImpl(g.getGroupId()); + try { + connection.exportObject(object); + } catch (DBusException e) { + e.printStackTrace(); + } + this.groups.add(new StructGroup(new DBusPath(object.getObjectPath()), + g.getGroupId().serialize(), + emptyIfNull(g.getTitle()))); + }); + } + + private void unExportGroups() { + this.groups.stream().map(StructGroup::getObjectPath).map(DBusPath::getPath).forEach(connection::unExportObject); + this.groups.clear(); + } + public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device { private final org.asamk.signal.manager.api.Device device; @@@ -878,4 -919,168 +943,168 @@@ } } } + + public class DbusSignalGroupImpl extends DbusProperties implements Signal.Group { + + private final GroupId groupId; + + public DbusSignalGroupImpl(final GroupId groupId) { + this.groupId = groupId; + super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group", + List.of(new DbusProperty<>("Id", groupId::serialize), + new DbusProperty<>("Name", () -> emptyIfNull(getGroup().getTitle()), this::setGroupName), + new DbusProperty<>("Description", + () -> emptyIfNull(getGroup().getDescription()), + this::setGroupDescription), + new DbusProperty<>("Avatar", this::setGroupAvatar), + new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked), + new DbusProperty<>("IsMember", () -> getGroup().isMember()), + new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()), + new DbusProperty<>("MessageExpirationTimer", + () -> getGroup().getMessageExpirationTimer(), + this::setMessageExpirationTime), + new DbusProperty<>("Members", + () -> new Variant<>(getRecipientStrings(getGroup().getMembers()), "as")), + new DbusProperty<>("PendingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().getPendingMembers()), "as")), + new DbusProperty<>("RequestingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().getRequestingMembers()), "as")), + new DbusProperty<>("Admins", + () -> new Variant<>(getRecipientStrings(getGroup().getAdminMembers()), "as")), + new DbusProperty<>("PermissionAddMember", + () -> getGroup().getPermissionAddMember().name(), + this::setGroupPermissionAddMember), + new DbusProperty<>("PermissionEditDetails", + () -> getGroup().getPermissionEditDetails().name(), + this::setGroupPermissionEditDetails), + new DbusProperty<>("PermissionSendMessage", + () -> getGroup().getPermissionSendMessage().name(), + this::setGroupPermissionSendMessage), + new DbusProperty<>("GroupInviteLink", () -> { + final var groupInviteLinkUrl = getGroup().getGroupInviteLinkUrl(); + return groupInviteLinkUrl == null ? "" : groupInviteLinkUrl.getUrl(); + })))); + } + + @Override + public String getObjectPath() { + return getGroupObjectPath(objectPath, groupId.serialize()); + } + + @Override + public void quitGroup() throws Error.Failure { + try { + m.quitGroup(groupId, Set.of()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (LastGroupAdminException e) { + throw new Error.LastGroupAdmin(e.getMessage()); + } + } + + @Override + public void addMembers(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withMembers(memberIdentifiers).build()); + } + + @Override + public void removeMembers(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withRemoveMembers(memberIdentifiers).build()); + } + + @Override + public void addAdmins(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withAdmins(memberIdentifiers).build()); + } + + @Override + public void removeAdmins(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withRemoveAdmins(memberIdentifiers).build()); + } + + @Override + public void resetLink() throws Error.Failure { + updateGroup(UpdateGroup.newBuilder().withResetGroupLink(true).build()); + } + + @Override + public void disableLink() throws Error.Failure { + updateGroup(UpdateGroup.newBuilder().withGroupLinkState(GroupLinkState.DISABLED).build()); + } + + @Override + public void enableLink(final boolean requiresApproval) throws Error.Failure { + updateGroup(UpdateGroup.newBuilder() + .withGroupLinkState(requiresApproval + ? GroupLinkState.ENABLED_WITH_APPROVAL + : GroupLinkState.ENABLED) + .build()); + } + + private org.asamk.signal.manager.api.Group getGroup() { + return m.getGroup(groupId); + } + + private void setGroupName(final String name) { + updateGroup(UpdateGroup.newBuilder().withName(name).build()); + } + + private void setGroupDescription(final String description) { + updateGroup(UpdateGroup.newBuilder().withDescription(description).build()); + } + + private void setGroupAvatar(final String avatar) { + updateGroup(UpdateGroup.newBuilder().withAvatarFile(new File(avatar)).build()); + } + + private void setMessageExpirationTime(final int expirationTime) { + updateGroup(UpdateGroup.newBuilder().withExpirationTimer(expirationTime).build()); + } + + private void setGroupPermissionAddMember(final String permission) { + updateGroup(UpdateGroup.newBuilder().withAddMemberPermission(GroupPermission.valueOf(permission)).build()); + } + + private void setGroupPermissionEditDetails(final String permission) { + updateGroup(UpdateGroup.newBuilder() + .withEditDetailsPermission(GroupPermission.valueOf(permission)) + .build()); + } + + private void setGroupPermissionSendMessage(final String permission) { + updateGroup(UpdateGroup.newBuilder() + .withIsAnnouncementGroup(GroupPermission.valueOf(permission) == GroupPermission.ONLY_ADMINS) + .build()); + } + + private void setIsBlocked(final boolean isBlocked) { + try { + m.setGroupBlocked(groupId, isBlocked); + } catch (NotMasterDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); + } catch (GroupNotFoundException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + private void updateGroup(final UpdateGroup updateGroup) { + try { + m.updateGroup(groupId, updateGroup); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } + } + } }