From: AsamK Date: Sat, 30 Oct 2021 10:46:59 +0000 (+0200) Subject: Merge branch 'master' into dbus_sendviewed X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/cd29144e81701698092f3334bee0c99c0f77f202?hp=-c Merge branch 'master' into dbus_sendviewed --- cd29144e81701698092f3334bee0c99c0f77f202 diff --combined man/signal-cli-dbus.5.adoc index 093f5807,4c317db3..1bc0b879 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@@ -29,15 -29,18 +29,18 @@@ method(arg1, arg2, ...) -> Where is according to DBus specification: - * : String - * : Byte Array - * : Array of Byte Arrays - * : String Array - * : Array of signed 64 bit integer - * : Boolean (0|1) - * : Signed 64 bit integer + * : Array of ... (comma-separated list) (array:) + * (...) : Struct (cannot be sent via `dbus-send`) + * : Boolean (false|true) (boolean:) + * : Signed 32-bit (int) integer (int32:) + * : DBusPath object (objpath:) + * : String (string:) + * : Signed 64-bit (long) integer (int64:) + * : Unsigned 8-bit (byte) integer (byte:) * <> : no return value + The final parenthetical value (such as "boolean:") is the type indicator used by `dbus-send`. + Exceptions are the names of the Java Exceptions returned in the body field. They typically contain an additional message with details. All Exceptions begin with "org.asamk.Signal.Error." which is omitted here for better readability. Phone numbers always have the format + @@@ -45,9 -48,9 +48,9 @@@ == 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:: @@@ -55,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 @@@ -86,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: GroupNotFound + Exceptions: Failure - joinGroup(inviteURI) -> <>:: - * inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App + enableLink(requiresApproval) -> <>:: + * requiresApproval : true=add numbers using the link to the requesting members list + + Enables the group's invitation link. + + Exceptions: Failure + + quitGroup() -> <>:: + Exceptions: Failure, LastGroupAdmin + + removeAdmins(recipients) -> <>:: + * recipients : String array of phone numbers + + Remove admin privileges from recipients. + + Exceptions: Failure + + removeMembers(recipients) -> <>:: + * recipients : String array of phone numbers + + Remove recipients from group. Exceptions: Failure + resetLink() -> <>:: + + Resets the group's invitation link to a new random URL starting with https://signal.group/# + + Exceptions: Failure + + === Deprecated group control methods + The following deprecated methods listen to the recipient's object path, which is constructed as follows: + "/org/asamk/Signal/" + DBusNumber + * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) + + getGroupIds() -> groupList:: + groupList : Array of Byte arrays representing the internal group identifiers + + All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() + + Exceptions: None + + getGroupName(groupId) -> groupName:: + * groupId : Byte array representing the internal group identifier + * groupName : The display name of the group + + Exceptions: None, if the group name is not found an empty string is returned + + isGroupBlocked(groupId) -> isGroupBlocked:: + * groupId : Byte array representing the internal group identifier + * isGroupBlocked : true=group is blocked; false=group is not blocked + + Dbus will not forward messages from a group when you have blocked it. + + Exceptions: InvalidGroupId, Failure + + isMember(groupId) -> isMember:: + * groupId : Byte array representing the internal group identifier + * isMember : true=you are a group member; false=you are not a group member + + Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) + quitGroup(groupId) -> <>:: * groupId : Byte array representing the internal group identifier Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember() - Exceptions: GroupNotFound, Failure + 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. - Exceptions: GroupNotFound, Failure, AttachmentInvalid + 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: InvalidUri sendContacts() -> <>:: @@@ -185,54 -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 - sendReadReceipt(recipient, targetSentTimestamp) -> <>:: - * recipient : Phone number of a single recipient - * targetSentTimestamp : Array of Longs to identify the corresponding signal messages + === Other methods - Exceptions: Failure, UntrustedIdentity + 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 - sendViewedReceipt(recipient, targetSentTimestamp) -> <>:: - * recipient : Phone number of a single recipient - * targetSentTimestamp : Array of Longs to identify the corresponding signal messages + Exceptions: None - Exceptions: Failure, UntrustedIdentity + getContactNumber(name) -> numbers:: + * numbers : Array of phone number + * name : Contact or profile name ("firstname lastname") - 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 + 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: Failure, InvalidNumber, GroupNotFound + 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 + + 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:: @@@ -242,19 -443,25 +443,31 @@@ * 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, AttachmentInvalid - Exceptions: Failure, GroupNotFound + sendReadReceipt(recipient, targetSentTimestamps) -> <>:: + * recipient : Phone number of a single recipient + * targetSentTimestamps : Array of Longs to identify the corresponding Signal messages + + Exceptions: Failure, UntrustedIdentity + ++sendViewedReceipt(recipient, targetSentTimestamp) -> <>:: ++* recipient : Phone number of a single recipient ++* targetSentTimestamp : Array of Longs to identify the corresponding signal messages ++ ++Exceptions: Failure, UntrustedIdentity + sendRemoteDeleteMessage(targetSentTimestamp, recipient) -> timestamp:: sendRemoteDeleteMessage(targetSentTimestamp, recipients) -> timestamp:: * targetSentTimestamp : Long representing timestamp of the message to delete @@@ -266,111 -473,78 +479,78 @@@ 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 master 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 : String array of linked devices - - 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 - Exception: Failure + Exceptions: Failure + + version() -> version:: + * version : Version string of signal-cli + + Exceptions: None == Signals + SyncMessageReceived (timestamp, sender, destination, groupId, message, attachments):: + * timestamp : Integer value that can be used to associate this e.g. with a sendMessage() + * 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/) - SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: The sync message is received when the user sends a message from a linked device. ReceiptReceived (timestamp, sender):: @@@ -384,7 -558,7 +564,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 dad7d5d7,240930dc..18bdbdb5 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@@ -1,7 -1,10 +1,10 @@@ package org.asamk; + import org.asamk.signal.commands.exceptions.IOErrorException; import org.freedesktop.dbus.DBusPath; + import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.annotations.DBusProperty; + import org.freedesktop.dbus.annotations.Position; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; @@@ -34,10 -37,6 +37,10 @@@ public interface Signal extends DBusInt String recipient, List messageIds ) throws Error.Failure, Error.UntrustedIdentity; + void sendViewedReceipt( + String recipient, List messageIds + ) throws Error.Failure, Error.UntrustedIdentity; + long sendRemoteDeleteMessage( long targetSentTimestamp, String recipient ) throws Error.Failure, Error.InvalidNumber; @@@ -84,14 -83,27 +87,27 @@@ 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; @@@ -106,7 -118,7 +122,7 @@@ DBusPath getDevice(long deviceId); - List listDevices() throws Error.Failure; + List listDevices() throws Error.Failure; DBusPath getThisDevice(); @@@ -133,18 -145,23 +149,23 @@@ 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 submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException; + class MessageReceived extends DBusSignal { private final long timestamp; @@@ -262,7 -279,37 +283,37 @@@ } } - @DBusProperty(name = "Id", type = Integer.class, access = DBusProperty.Access.READ) + class StructDevice extends Struct { + + @Position(0) + DBusPath objectPath; + + @Position(1) + Long id; + + @Position(2) + String name; + + public StructDevice(final DBusPath objectPath, final Long id, final String name) { + this.objectPath = objectPath; + this.id = id; + this.name = name; + } + + public DBusPath getObjectPath() { + return objectPath; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + + @DBusProperty(name = "Id", type = Long.class, access = DBusProperty.Access.READ) @DBusProperty(name = "Name", type = String.class) @DBusProperty(name = "Created", type = String.class, access = DBusProperty.Access.READ) @DBusProperty(name = "LastSeen", type = String.class, access = DBusProperty.Access.READ) @@@ -271,54 -318,137 +322,137 @@@ 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 { public AttachmentInvalid(final String message) { - super(message); + super("Invalid attachment: " + message); } } class InvalidUri extends DBusExecutionException { public InvalidUri(final String message) { - super(message); + super("Invalid uri: " + message); } } class Failure extends DBusExecutionException { + public Failure(final Exception e) { + super("Failure: " + e.getMessage() + " (" + e.getClass().getSimpleName() + ")"); + } + public Failure(final String message) { - super(message); + super("Failure: " + message); + } + } + + class DeviceNotFound extends DBusExecutionException { + + public DeviceNotFound(final String message) { + super("Device not found: " + message); } } class GroupNotFound extends DBusExecutionException { public GroupNotFound(final String message) { - super(message); + super("Group not found: " + message); } } class InvalidGroupId extends DBusExecutionException { public InvalidGroupId(final String message) { - super(message); + super("Invalid group id: " + message); + } + } + + class LastGroupAdmin extends DBusExecutionException { + + public LastGroupAdmin(final String message) { + super("Last group admin: " + message); } } class InvalidNumber extends DBusExecutionException { public InvalidNumber(final String message) { - super(message); + super("Invalid number: " + message); } } class UntrustedIdentity extends DBusExecutionException { public UntrustedIdentity(final String message) { - super(message); + super("Untrusted identity: " + message); } } } diff --combined src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 18565a0d,7be6a8e2..977dfa75 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@@ -2,6 -2,7 +2,7 @@@ package org.asamk.signal.dbus import org.asamk.Signal; import org.asamk.signal.BaseConfig; + import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@@ -11,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; @@@ -24,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; @@@ -38,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; @@@ -55,7 -62,8 +62,8 @@@ public class DbusSignalImpl implements private final String objectPath; private DBusPath thisDevice; - private final List devices = new ArrayList<>(); + 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; @@@ -65,10 -73,12 +73,12 @@@ public void initObjects() { updateDevices(); + updateGroups(); } public void close() { unExportDevices(); + unExportGroups(); } @Override @@@ -81,6 -91,18 +91,18 @@@ return m.getSelfNumber(); } + @Override + public void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException { + final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", ""); + + try { + m.submitRateLimitRecaptchaChallenge(challenge, captcha); + } catch (IOException e) { + throw new IOErrorException("Submit challenge error: " + e.getMessage(), e); + } + + } + @Override public void addDevice(String uri) { try { @@@ -97,45 -119,19 +119,19 @@@ @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 - public List listDevices() { + public List listDevices() { updateDevices(); return this.devices; } - private void updateDevices() { - List linkedDevices; - try { - linkedDevices = m.getLinkedDevices(); - } catch (IOException | Error.Failure e) { - throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); - } - - unExportDevices(); - - linkedDevices.forEach(d -> { - final var object = new DbusSignalDeviceImpl(d); - final var deviceObjectPath = object.getObjectPath(); - try { - connection.exportObject(object); - } catch (DBusException e) { - e.printStackTrace(); - } - if (d.isThisDevice()) { - thisDevice = new DBusPath(deviceObjectPath); - } - this.devices.add(new DBusPath(deviceObjectPath)); - }); - } - - private void unExportDevices() { - this.devices.stream().map(DBusPath::getPath).forEach(connection::unExportObject); - this.devices.clear(); - } - @Override public DBusPath getThisDevice() { updateDevices(); @@@ -157,12 -153,12 +153,12 @@@ .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results.timestamp(), results.results()); + return results.timestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { - throw new Error.Failure(e.getMessage()); + throw new Error.Failure(e); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } @@@ -186,8 -182,8 +182,8 @@@ getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results.timestamp(), results.results()); + return results.timestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { @@@ -202,8 -198,8 +198,8 @@@ try { final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results.timestamp(), results.results()); + return results.timestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { @@@ -240,8 -236,8 +236,8 @@@ getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results.timestamp(), results.results()); + return results.timestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { @@@ -282,19 -278,6 +278,19 @@@ } } + @Override + public void sendViewedReceipt( + final String recipient, final List messageIds + ) throws Error.Failure, Error.UntrustedIdentity { + try { + m.sendViewedReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()), messageIds); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (UntrustedIdentityException e) { + throw new Error.UntrustedIdentity(e.getMessage()); + } + } + @Override public void sendContacts() { try { @@@ -320,8 -303,8 +316,8 @@@ try { final var results = m.sendMessage(new Message(message, attachments), Set.of(RecipientIdentifier.NoteToSelf.INSTANCE)); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results.timestamp(), results.results()); + return results.timestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { @@@ -335,7 -318,7 +331,7 @@@ public void sendEndSessionMessage(final List recipients) { try { final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); - checkSendMessageResults(results.getTimestamp(), results.getResults()); + checkSendMessageResults(results.timestamp(), results.results()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } @@@ -346,8 -329,8 +342,8 @@@ try { var results = m.sendMessage(new Message(message, attachments), Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results.timestamp(), results.results()); + return results.timestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { @@@ -371,8 -354,8 +367,8 @@@ getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetSentTimestamp, Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); - checkSendMessageResults(results.getTimestamp(), results.getResults()); - return results.getTimestamp(); + checkSendMessageResults(results.timestamp(), results.results()); + return results.timestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { @@@ -423,6 -406,8 +419,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) { @@@ -435,18 -420,34 +433,34 @@@ var groups = m.getGroups(); var ids = new ArrayList(groups.size()); for (var group : groups) { - ids.add(group.getGroupId().serialize()); + ids.add(group.groupId().serialize()); } 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)); - if (group == null || group.getTitle() == null) { + if (group == null || group.title() == null) { return ""; } else { - return group.getTitle(); + return group.title(); } } @@@ -456,10 -457,18 +470,18 @@@ if (group == null) { return List.of(); } else { - return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); + final var members = group.members(); + 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 { @@@ -469,25 -478,17 +491,17 @@@ 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()); + checkSendMessageResults(results.second().timestamp(), results.second().results()); 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()); + checkSendMessageResults(results.timestamp(), results.results()); } return groupId; } @@@ -599,8 -600,8 +613,8 @@@ // all numbers the system knows @Override public List listNumbers() { - return Stream.concat(m.getIdentities().stream().map(Identity::getRecipient), - m.getContacts().stream().map(Pair::first)) + return Stream.concat(m.getIdentities().stream().map(Identity::recipient), + m.getContacts().stream().map(Pair::first)) .map(a -> a.getNumber().orElse(null)) .filter(Objects::nonNull) .distinct() @@@ -619,7 -620,7 +633,7 @@@ } // Try profiles if no contact name was found for (var identity : m.getIdentities()) { - final var address = identity.getRecipient(); + final var address = identity.recipient(); var number = address.getNumber().orElse(null); if (number != null) { Profile profile = null; @@@ -765,6 -766,10 +779,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 { @@@ -801,35 -806,101 +819,101 @@@ return name.isEmpty() ? null : name; } + private String emptyIfNull(final String string) { + return string == null ? "" : string; + } + private static String getDeviceObjectPath(String basePath, long deviceId) { return basePath + "/Devices/" + deviceId; } + private void updateDevices() { + List linkedDevices; + try { + linkedDevices = m.getLinkedDevices(); + } catch (IOException e) { + throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); + } + + unExportDevices(); + + linkedDevices.forEach(d -> { + final var object = new DbusSignalDeviceImpl(d); + final var deviceObjectPath = object.getObjectPath(); + try { + connection.exportObject(object); + } catch (DBusException e) { + e.printStackTrace(); + } + if (d.isThisDevice()) { + thisDevice = new DBusPath(deviceObjectPath); + } + this.devices.add(new StructDevice(new DBusPath(deviceObjectPath), d.id(), emptyIfNull(d.name()))); + }); + } + + private void unExportDevices() { + this.devices.stream() + .map(StructDevice::getObjectPath) + .map(DBusPath::getPath) + .forEach(connection::unExportObject); + 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.groupId()); + try { + connection.exportObject(object); + } catch (DBusException e) { + e.printStackTrace(); + } + this.groups.add(new StructGroup(new DBusPath(object.getObjectPath()), + g.groupId().serialize(), + emptyIfNull(g.title()))); + }); + } + + 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; 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)))); + List.of(new DbusProperty<>("Id", device::id), + new DbusProperty<>("Name", () -> emptyIfNull(device.name()), this::setDeviceName), + new DbusProperty<>("Created", device::created), + new DbusProperty<>("LastSeen", device::lastSeen)))); this.device = device; } @Override public String getObjectPath() { - return getDeviceObjectPath(objectPath, device.getId()); + return getDeviceObjectPath(objectPath, device.id()); } @Override public void removeDevice() throws Error.Failure { try { - m.removeLinkedDevices(device.getId()); + m.removeLinkedDevices(device.id()); updateDevices(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); @@@ -849,4 -920,168 +933,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().title()), this::setGroupName), + new DbusProperty<>("Description", + () -> emptyIfNull(getGroup().description()), + 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().messageExpirationTimer(), + this::setMessageExpirationTime), + new DbusProperty<>("Members", + () -> new Variant<>(getRecipientStrings(getGroup().members()), "as")), + new DbusProperty<>("PendingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().pendingMembers()), "as")), + new DbusProperty<>("RequestingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().requestingMembers()), "as")), + new DbusProperty<>("Admins", + () -> new Variant<>(getRecipientStrings(getGroup().adminMembers()), "as")), + new DbusProperty<>("PermissionAddMember", + () -> getGroup().permissionAddMember().name(), + this::setGroupPermissionAddMember), + new DbusProperty<>("PermissionEditDetails", + () -> getGroup().permissionEditDetails().name(), + this::setGroupPermissionEditDetails), + new DbusProperty<>("PermissionSendMessage", + () -> getGroup().permissionSendMessage().name(), + this::setGroupPermissionSendMessage), + new DbusProperty<>("GroupInviteLink", () -> { + final var groupInviteLinkUrl = getGroup().groupInviteLinkUrl(); + 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()); + } + } + } } diff --combined src/main/java/org/asamk/signal/json/JsonReceiptMessage.java index 15e2cf43,af7050b1..1199c790 --- a/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java @@@ -1,47 -1,20 +1,21 @@@ package org.asamk.signal.json; - import com.fasterxml.jackson.annotation.JsonProperty; - import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import java.util.List; - class JsonReceiptMessage { - - @JsonProperty - final long when; - - @JsonProperty - final boolean isDelivery; - - @JsonProperty - final boolean isRead; - - @JsonProperty - final boolean isViewed; - - @JsonProperty - final List timestamps; - - JsonReceiptMessage(SignalServiceReceiptMessage receiptMessage) { - this.when = receiptMessage.getWhen(); - this.isDelivery = receiptMessage.isDeliveryReceipt(); - this.isRead = receiptMessage.isReadReceipt(); - this.isViewed = receiptMessage.isViewedReceipt(); - this.timestamps = receiptMessage.getTimestamps(); - } -record JsonReceiptMessage(long when, boolean isDelivery, boolean isRead, List timestamps) { ++record JsonReceiptMessage(long when, boolean isDelivery, boolean isRead, boolean isViewed, List timestamps) { - private JsonReceiptMessage( - final long when, final boolean isDelivery, final boolean isRead, final boolean isViewed, final List timestamps - ) { - this.when = when; - this.isDelivery = isDelivery; - this.isRead = isRead; - this.isViewed = isViewed; - this.timestamps = timestamps; + static JsonReceiptMessage from(SignalServiceReceiptMessage receiptMessage) { + final var when = receiptMessage.getWhen(); + final var isDelivery = receiptMessage.isDeliveryReceipt(); + final var isRead = receiptMessage.isReadReceipt(); ++ final var isViewed = receiptMessage.isViewedReceipt(); + final var timestamps = receiptMessage.getTimestamps(); - return new JsonReceiptMessage(when, isDelivery, isRead, timestamps); ++ return new JsonReceiptMessage(when, isDelivery, isRead, isViewed, timestamps); } static JsonReceiptMessage deliveryReceipt(final long when, final List timestamps) { - return new JsonReceiptMessage(when, true, false, false, timestamps); - return new JsonReceiptMessage(when, true, false, timestamps); ++ return new JsonReceiptMessage(when, true, false, false, false, timestamps); } }