]> nmode's Git Repositories - signal-cli/commitdiff
Merge branch 'master' into dbus_sendviewed 764/head
authorAsamK <asamk@gmx.de>
Sat, 30 Oct 2021 10:46:59 +0000 (12:46 +0200)
committerGitHub <noreply@github.com>
Sat, 30 Oct 2021 10:46:59 +0000 (12:46 +0200)
126 files changed:
.github/workflows/ci.yml
.github/workflows/codeql-analysis.yml
.idea/codeStyles/Project.xml
CHANGELOG.md
README.md
build.gradle.kts
graalvm-config-dir/jni-config.json
graalvm-config-dir/proxy-config.json
graalvm-config-dir/reflect-config.json
graalvm-config-dir/resource-config.json
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties
gradlew
lib/build.gradle.kts
lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java
lib/src/main/java/org/asamk/signal/manager/JsonStickerPack.java
lib/src/main/java/org/asamk/signal/manager/LibSignalLogger.java
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
lib/src/main/java/org/asamk/signal/manager/PathConfig.java
lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java
lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java
lib/src/main/java/org/asamk/signal/manager/TrustLevel.java
lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java
lib/src/main/java/org/asamk/signal/manager/api/Device.java
lib/src/main/java/org/asamk/signal/manager/api/Group.java
lib/src/main/java/org/asamk/signal/manager/api/Identity.java
lib/src/main/java/org/asamk/signal/manager/api/Message.java
lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java
lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java
lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java
lib/src/main/java/org/asamk/signal/manager/api/TypingAction.java
lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java
lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java
lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java
lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java
lib/src/main/java/org/asamk/signal/manager/groups/GroupId.java
lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java
lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java
lib/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java
lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java [deleted file]
lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java [deleted file]
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java
lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java
lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java
lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java
lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java
lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java
lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java
lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java
lib/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java
lib/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java
lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
lib/src/main/java/org/asamk/signal/manager/util/StickerUtils.java
man/signal-cli-dbus.5.adoc
man/signal-cli.1.adoc
run_tests.sh
src/main/java/org/asamk/Signal.java
src/main/java/org/asamk/SignalControl.java
src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java
src/main/java/org/asamk/signal/ReceiveMessageHandler.java
src/main/java/org/asamk/signal/commands/BlockCommand.java
src/main/java/org/asamk/signal/commands/DaemonCommand.java
src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java
src/main/java/org/asamk/signal/commands/JoinGroupCommand.java
src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java
src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java
src/main/java/org/asamk/signal/commands/ListContactsCommand.java
src/main/java/org/asamk/signal/commands/ListDevicesCommand.java
src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
src/main/java/org/asamk/signal/commands/ReceiveCommand.java
src/main/java/org/asamk/signal/commands/RegisterCommand.java
src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java
src/main/java/org/asamk/signal/commands/SendCommand.java
src/main/java/org/asamk/signal/commands/SendReactionCommand.java
src/main/java/org/asamk/signal/commands/UnblockCommand.java
src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java
src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
src/main/java/org/asamk/signal/dbus/DbusProperties.java
src/main/java/org/asamk/signal/dbus/DbusProperty.java
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
src/main/java/org/asamk/signal/json/JsonAttachment.java
src/main/java/org/asamk/signal/json/JsonCallMessage.java
src/main/java/org/asamk/signal/json/JsonContactAddress.java
src/main/java/org/asamk/signal/json/JsonContactAvatar.java
src/main/java/org/asamk/signal/json/JsonContactEmail.java
src/main/java/org/asamk/signal/json/JsonContactName.java
src/main/java/org/asamk/signal/json/JsonContactPhone.java
src/main/java/org/asamk/signal/json/JsonDataMessage.java
src/main/java/org/asamk/signal/json/JsonError.java
src/main/java/org/asamk/signal/json/JsonGroupInfo.java
src/main/java/org/asamk/signal/json/JsonMention.java
src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
src/main/java/org/asamk/signal/json/JsonQuote.java
src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java
src/main/java/org/asamk/signal/json/JsonReaction.java
src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
src/main/java/org/asamk/signal/json/JsonRemoteDelete.java
src/main/java/org/asamk/signal/json/JsonSharedContact.java
src/main/java/org/asamk/signal/json/JsonSticker.java
src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java
src/main/java/org/asamk/signal/json/JsonSyncMessage.java
src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java
src/main/java/org/asamk/signal/json/JsonTypingMessage.java
src/main/java/org/asamk/signal/jsonrpc/JsonRpcBulkMessage.java
src/main/java/org/asamk/signal/jsonrpc/JsonRpcMessage.java
src/main/java/org/asamk/signal/jsonrpc/JsonRpcRequest.java
src/main/java/org/asamk/signal/jsonrpc/JsonRpcResponse.java
src/main/java/org/asamk/signal/util/ErrorUtils.java

index 28f5039ee3622d720b0e6c21e338d915b02cd8cd..ff73584b53833c2e48239f02aa55abf116ccfe07 100644 (file)
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        java: [ '11', '16' ]
+        java: [ '17' ]
 
     steps:
       - uses: actions/checkout@v1
index c55e656db3035e5fc6bd335a87507abae500c625..0ef62f7e5b7790deffe9fb408c3a46ec2816aff3 100644 (file)
@@ -19,7 +19,7 @@ jobs:
       - name: Setup Java JDK
         uses: actions/setup-java@v1
         with:
-          java-version: 11
+          java-version: 17
 
       - name: Checkout repository
         uses: actions/checkout@v2
index e2bacc4894bff166c53ec8b630272524cd3e3ae3..9cbd20b7cade6244c927163495c98d2408ec8fc9 100644 (file)
           <emptyLine />
         </value>
       </option>
+      <option name="RECORD_COMPONENTS_WRAP" value="5" />
+      <option name="NEW_LINE_AFTER_LPAREN_IN_RECORD_HEADER" value="true" />
+      <option name="RPAREN_ON_NEW_LINE_IN_RECORD_HEADER" value="true" />
       <option name="JD_P_AT_EMPTY_LINES" value="false" />
     </JavaCodeStyleSettings>
     <JetCodeStyleSettings>
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>
-    <XML>
-      <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
-    </XML>
     <codeStyleSettings language="JAVA">
       <option name="RIGHT_MARGIN" value="120" />
       <option name="KEEP_LINE_BREAKS" value="false" />
@@ -52,6 +52,7 @@
       <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
       <option name="TERNARY_OPERATION_WRAP" value="5" />
       <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+      <option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
       <option name="ENUM_CONSTANTS_WRAP" value="2" />
     </codeStyleSettings>
     <codeStyleSettings language="XML">
index 26b96784eb365b7e26c86424ddb6e0570657513a..3e60c8a4f13667ee2f3a95392dbf0128846463c9 100644 (file)
@@ -1,6 +1,37 @@
 # Changelog
 
 ## [Unreleased]
+**Attention**: Now requires Java 17
+
+## [0.9.2] - 2021-10-24
+### Fixed
+- dbus `listNumbers` method works again
+
+### Changed
+- Improved provisioning error handling if the last steps fail
+- Adapt behavior of receive command as dbus client to match normal mode
+- Update captcha url for proof required handling
+
+## [0.9.1] - 2021-10-16
+**Attention**: Now requires native libzkgroup version 0.8
+
+### Added
+- New command `updateConfiguration` which allows setting configurations for linked devices
+- Improved dbus daemon for group handling, groups are now exported as separate dbus objects
+- Linked devices can be managed via dbus
+- New dbus methods sendTyping and sendReadReceipt (Thanks @JtheSaw)
+- New dbus methods submitRateLimitChallenge, isRegistered, listDevices, setExpirationTimer, sendContacts, sendSyncRequest, uploadStickerPack, setPin and removePin (Thanks @John Freed)
+- New dbus method getSelfNumber
+
+### Fixed
+- Do not send message resend request to own device
+- Allow message from pending member to accept group invitations
+- Fix issue which could cause signal-cli to repeatedly send the same delivery receipts
+- Reconnect websocket after connection loss
+
+### Changed
+- Use new provisioning URL `sgnl://linkdevice` instead of `tsdevice:/`
+- The gradle command to build a graalvm native image is now `./gradlew nativeCompile`
 
 ## [0.9.0] - 2021-09-12
 **Attention**: Now requires native libsignal-client version 0.9
@@ -189,6 +220,142 @@ See https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal fo
 ### Fixed
 - Issue where some messages were sent with an old counter index
 
-## Older
-
-Look at the [release tags](https://github.com/AsamK/signal-cli/releases) for information about older releases.
+## [0.6.11] - 2020-10-14
+- Fix issue with receiving message reactions
+
+## [0.6.10] - 2020-09-11
+- Fix issue when retrieving profiles
+- Workaround issue with libzkgroup on platforms other than linux x86_64
+
+## [0.6.9] - 2020-09-10
+- Minor bug fixes and improvements
+- dbus functionality now works on FreeBSD
+- signal-cli now requires Java 11
+
+**Warning: this version only works on Linux x86_64, will be fixed in 0.6.10**
+
+## [0.6.8] - 2020-05-22
+- Switch to hypfvieh dbus-java, which doesn't require a native library anymore (drops requirement of libmatthew-unix-java)
+- Bugfixes for messages with uuids
+- Add `--expiration` parameter to `updateContact` command to set expiration timer
+
+## [0.6.7] - 2020-04-03
+- Send command now returns the timestamp of the sent message
+- DBus daemon: Publish received sync message to SyncMessageReceived signal
+- Fix issue with resolving e164/uuid addresses for sessions
+- Fix pack key length for sticker upload
+
+## [0.6.6] - 2020-03-29
+- Added listContacts command
+- Added block/unblock commands to block contacts and groups
+- Added uploadStickerPack command to upload sticker packs (see man page for more details)
+- Full support for sending and receiving unidentified sender messages
+- Support for message reactions with emojis
+- Internal: support recipients with uuids
+
+## [0.6.5] - 2019-11-11
+Supports receiving messages sent with unidentified sender
+
+## [0.6.4] - 2019-11-02
+- Fix rounding error for attachment ids in json output
+- Add additional info to json output
+- Add commands to update profile name and avatar
+- Add command to update contact names
+
+## [0.6.3] - 2019-09-05
+Bug fixes and small improvements
+
+## [0.6.2] - 2018-12-16
+- Fixes sending of group messages
+
+## [0.6.1] - 2018-12-09
+- Added getGroupIds dbus command
+- Use "NativePRNG" pseudo random number generator, if available
+- Switch default data path:
+  `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)
+  Existing data paths will continue to work (used as fallback)
+
+## [0.6.0] - 2018-05-03
+- Simple json output
+- dbus signal for receiving messages
+- Registration lock PIN
+- Output quoted message
+
+## [0.5.6] - 2017-06-16
+* new listGroups command
+* Support for attachments with file names
+* Support for complete contacts sync
+* Support for contact verification sync 
+* DBus interface:
+ * Get/Set group info
+ * Get/Set contact info
+
+## [0.5.5] - 2017-02-18
+- fix receiving messages on linked devices
+- add unregister command
+
+## [0.5.4] - 2017-02-17
+- Fix linking of new devices
+
+## [0.5.3] - 2017-01-29
+* New commandline paramter for receive: --ignore-attachments
+* Updated dependencies
+
+## [0.5.2] - 2016-12-16
+- Add support for group info requests
+- Improve closing of file streams
+
+## [0.5.1] - 2016-11-18
+- Support new safety numbers (https://whispersystems.org/blog/safety-number-updates/)
+- Add a man page
+- Support sending disappearing messages, if the recipient has activated it
+
+## [0.5.0] - 2016-08-29
+- Check if a number is registered on Signal, before adding it to a group
+- Prevent sending to groups that the user has quit
+- Commands to trust new identity keys (see README)
+- Messages from untrusted identities are stored on disk and decrypted when the user trusts the identity
+- Timestamps shown in ISO 8601 format
+
+## [0.4.1] - 2016-07-18
+- Fix issue with creating groups
+- Lock config file to prevent parallel access by multiple instances of signal-cli
+- Improve return codes, always return non-zero code, when sending failed
+
+## [0.4.0] - 2016-06-19
+- Linking to Signal-Desktop and Signal-Android is now possible (Provisioning)
+- Added a contact store, mainly for syncing contacts with linked devices (editing not yet possible via cli)
+- Avatars for groups and contacts are now stored (new folder "avatars" in the config path)
+
+## [0.3.1] - 2016-04-03
+- Fix running with Oracle JRE 8
+- Fix registering
+- Fix unicode warning when compiling with non utf8 locale
+
+## [0.3.0] - 2016-04-02
+- Renamed textsecure-cli to signal-cli, following the rename of libtextsecure-java to libsignal-service-java
+- The experimental dbus interface was also renamed to org.asamk.Signal
+- Upload new prekeys to the server, when there are less than 20 left, prekeys are needed to create new sessions
+
+## [0.2.1] - 2016-02-10
+- Improve dbus service
+- New command line argument --config to specify config directory
+
+## [0.2.0] - 2015-12-30
+Added an experimental dbus interface, for sending and receiving messages (The interface is unstable and may change with future releases).
+
+This release works with Java 7 and 8.
+
+## [0.1.0] - 2015-11-28
+Add support for creating/updating groups and sending to them
+
+## [0.0.5] - 2015-11-21
+- Add receive timeout commandline parameter
+- Show message group info
+
+## [0.0.4] - 2015-09-22
+
+## [0.0.3] - 2015-08-07
+
+## [0.0.2] - 2015-07-08
+First release
index fe4358493af629a87dd8d445a3f1c9f6dbe3ac19..67bbb75ae955d25771e54d1c099fec73213e55ff 100644 (file)
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ It also has a JSON-RPC based interface, see the [documentation](https://github.c
 You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/) and there is a [FreeBSD port](https://www.freshports.org/net-im/signal-cli) available as well.
 
 System requirements:
-- at least Java Runtime Environment (JRE) 11
+- at least Java Runtime Environment (JRE) 17
 - native libraries: libzkgroup, libsignal-client
 
   Those are bundled for x86_64 Linux (with recent enough glibc, see #643), for other systems/architectures see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
@@ -81,15 +81,19 @@ dependencies. If you have a recent gradle version installed, you can replace `./
 
         ./gradlew build
 
-    3a. Create shell wrapper in *build/install/signal-cli/bin*:
+    2a. Create shell wrapper in *build/install/signal-cli/bin*:
 
         ./gradlew installDist
 
-    3b. Create tar file in *build/distributions*:
+    2b. Create tar file in *build/distributions*:
 
         ./gradlew distTar
 
-    3c. Compile and run signal-cli:
+    2c. Create a fat tar file in *build/libs/signal-cli-fat*:
+
+        ./gradlew fatJar
+
+    2d. Compile and run signal-cli:
 
         ./gradlew run --args="--help"
 
index 51b2ef755f9ab1f6ea0fa1f0ec773709b2b8411c..eb4b18fafc56d0b9b166d4e35b5053b3f8dc22bc 100644 (file)
@@ -3,14 +3,14 @@ plugins {
     application
     eclipse
     `check-lib-versions`
-    id("org.graalvm.buildtools.native") version "0.9.5"
+    id("org.graalvm.buildtools.native") version "0.9.6"
 }
 
-version = "0.9.0"
+version = "0.9.2"
 
 java {
-    sourceCompatibility = JavaVersion.VERSION_11
-    targetCompatibility = JavaVersion.VERSION_11
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
 }
 
 application {
@@ -22,6 +22,7 @@ graalvmNative {
         this["main"].run {
             configurationFileDirectories.from(file("graalvm-config-dir"))
             buildArgs.add("--allow-incomplete-classpath")
+            buildArgs.add("--report-unsupported-elements-at-runtime")
         }
     }
 }
@@ -34,8 +35,8 @@ repositories {
 dependencies {
     implementation("org.bouncycastle:bcprov-jdk15on:1.69")
     implementation("net.sourceforge.argparse4j:argparse4j:0.9.0")
-    implementation("com.github.hypfvieh:dbus-java:3.3.0")
-    implementation("org.slf4j:slf4j-simple:1.7.30")
+    implementation("com.github.hypfvieh:dbus-java:3.3.1")
+    implementation("org.slf4j:slf4j-simple:1.7.32")
     implementation(project(":lib"))
 }
 
@@ -64,3 +65,17 @@ tasks.withType<Jar> {
         )
     }
 }
+
+task("fatJar", type = Jar::class) {
+    archiveBaseName.set("${project.name}-fat")
+    exclude(
+        "META-INF/*.SF",
+        "META-INF/*.DSA",
+        "META-INF/*.RSA",
+        "META-INF/NOTICE",
+        "META-INF/LICENSE",
+        "**/module-info.class"
+    )
+    from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
+    with(tasks.jar.get() as CopySpec)
+}
index 8c8c30f5d075b9dd4ac3b9a4b07bc01a39e7d6b1..c4ebca361dedfc59145d2164586d7c043dbdb824 100644 (file)
@@ -1,29 +1,33 @@
 [
+{
+  "name":"java.lang.Boolean",
+  "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]}
+,
 {
   "name":"java.lang.ClassLoader",
   "methods":[
     {"name":"getPlatformClassLoader","parameterTypes":[] }, 
     {"name":"loadClass","parameterTypes":["java.lang.String"] }
-  ]
-},
+  ]}
+,
 {
   "name":"java.lang.IllegalStateException",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
+,
 {
-  "name":"java.lang.NoSuchMethodError"
-},
+  "name":"java.lang.NoSuchMethodError"}
+,
 {
   "name":"java.lang.UnsatisfiedLinkError",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
+,
 {
   "name":"java.util.UUID",
-  "methods":[{"name":"<init>","parameterTypes":["long","long"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["long","long"] }]}
+,
 {
-  "name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"
-},
+  "name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"}
+,
 {
   "name":"org.asamk.signal.manager.storage.protocol.SignalProtocolStore",
   "methods":[
     {"name":"saveIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.IdentityKey"] }, 
     {"name":"storeSenderKey","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","java.util.UUID","org.whispersystems.libsignal.groups.state.SenderKeyRecord"] }, 
     {"name":"storeSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.state.SessionRecord"] }
-  ]
-},
+  ]}
+,
 {
   "name":"org.graalvm.nativebridge.jni.JNIExceptionWrapperEntryPoints",
-  "methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]
-},
+  "methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]}
+,
 {
   "name":"org.whispersystems.libsignal.DuplicateMessageException",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
+,
 {
   "name":"org.whispersystems.libsignal.IdentityKey",
   "methods":[
     {"name":"<init>","parameterTypes":["byte[]"] }, 
     {"name":"serialize","parameterTypes":[] }
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.libsignal.IdentityKeyPair",
-  "methods":[{"name":"serialize","parameterTypes":[] }]
-},
+  "methods":[{"name":"serialize","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.libsignal.InvalidMessageException",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
+,
 {
   "name":"org.whispersystems.libsignal.SignalProtocolAddress",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.String","int"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String","int"] }]}
+,
 {
   "name":"org.whispersystems.libsignal.UntrustedIdentityException",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
+,
 {
   "name":"org.whispersystems.libsignal.groups.state.SenderKeyRecord",
   "methods":[
     {"name":"<init>","parameterTypes":["long"] }, 
     {"name":"nativeHandle","parameterTypes":[] }
-  ]
-},
+  ]}
+,
 {
-  "name":"org.whispersystems.libsignal.groups.state.SenderKeyStore"
-},
+  "name":"org.whispersystems.libsignal.groups.state.SenderKeyStore"}
+,
 {
   "name":"org.whispersystems.libsignal.logging.Log",
-  "methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]
-},
+  "methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]}
+,
 {
   "name":"org.whispersystems.libsignal.protocol.PlaintextContent",
-  "methods":[{"name":"nativeHandle","parameterTypes":[] }]
-},
+  "fields":[{"name":"unsafeHandle"}],
+  "methods":[{"name":"nativeHandle","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.libsignal.protocol.PreKeySignalMessage",
+  "fields":[{"name":"unsafeHandle"}],
   "methods":[
     {"name":"<init>","parameterTypes":["long"] }, 
     {"name":"nativeHandle","parameterTypes":[] }
-  ]
-},
+  ]}
+,
 {
-  "name":"org.whispersystems.libsignal.protocol.SenderKeyMessage"
-},
+  "name":"org.whispersystems.libsignal.protocol.SenderKeyMessage"}
+,
 {
   "name":"org.whispersystems.libsignal.protocol.SignalMessage",
+  "fields":[{"name":"unsafeHandle"}],
   "methods":[
     {"name":"<init>","parameterTypes":["long"] }, 
     {"name":"nativeHandle","parameterTypes":[] }
-  ]
-},
+  ]}
+,
 {
-  "name":"org.whispersystems.libsignal.state.IdentityKeyStore"
-},
+  "name":"org.whispersystems.libsignal.state.IdentityKeyStore"}
+,
 {
   "name":"org.whispersystems.libsignal.state.IdentityKeyStore$Direction",
   "fields":[
     {"name":"RECEIVING"}, 
     {"name":"SENDING"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.libsignal.state.PreKeyRecord",
-  "methods":[{"name":"nativeHandle","parameterTypes":[] }]
-},
+  "fields":[{"name":"unsafeHandle"}],
+  "methods":[{"name":"nativeHandle","parameterTypes":[] }]}
+,
 {
-  "name":"org.whispersystems.libsignal.state.PreKeyStore"
-},
+  "name":"org.whispersystems.libsignal.state.PreKeyStore"}
+,
 {
   "name":"org.whispersystems.libsignal.state.SessionRecord",
+  "fields":[{"name":"unsafeHandle"}],
   "methods":[
     {"name":"<init>","parameterTypes":["byte[]"] }, 
     {"name":"nativeHandle","parameterTypes":[] }
-  ]
-},
+  ]}
+,
 {
-  "name":"org.whispersystems.libsignal.state.SessionStore"
-},
+  "name":"org.whispersystems.libsignal.state.SessionStore"}
+,
 {
   "name":"org.whispersystems.libsignal.state.SignedPreKeyRecord",
-  "methods":[{"name":"nativeHandle","parameterTypes":[] }]
-},
+  "fields":[{"name":"unsafeHandle"}],
+  "methods":[{"name":"nativeHandle","parameterTypes":[] }]}
+,
 {
-  "name":"org.whispersystems.libsignal.state.SignedPreKeyStore"
-}
+  "name":"org.whispersystems.libsignal.state.SignedPreKeyStore"}
+
 ]
index 7abe9244f1b275b82f60bff2889ec7ac317e0e37..be8f8d3ceacf6478c808d0d9b32fdc26a5e1e15e 100644 (file)
@@ -1,4 +1,8 @@
 [
-  ["org.asamk.Signal"],
-  ["org.freedesktop.dbus.interfaces.DBus"]
+  {
+    "interfaces":["org.asamk.Signal"]}
+  ,
+  {
+    "interfaces":["org.freedesktop.dbus.interfaces.DBus"]}
+  
 ]
index 819eafa7b11b9a53f1c2569ee85cac75ed07e2d0..d247203518c670fc7d324ddbd19c9ad74054981a 100644 (file)
 [
+{
+  "name":"[B",
+  "queryAllDeclaredMethods":true,
+  "queryAllPublicMethods":true}
+,
+{
+  "name":"[C"}
+,
+{
+  "name":"[I",
+  "queryAllDeclaredMethods":true,
+  "queryAllPublicMethods":true}
+,
+{
+  "name":"[J"}
+,
+{
+  "name":"[Lorg.whispersystems.signalservice.api.groupsv2.TemporalCredential;"}
+,
 {
   "name":"byte[]",
   "allDeclaredMethods":true,
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
-  "name":"char[]"
-},
+  "name":"char[]"}
+,
+{
+  "name":"com.fasterxml.jackson.databind.ext.Java7HandlersImpl",
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"com.google.protobuf.AbstractProtobufList",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"com.google.protobuf.Internal$LongList",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"com.google.protobuf.Internal$ProtobufList",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"com.google.protobuf.LongArrayList",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"com.google.protobuf.PrimitiveNonBoxingCollection",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"com.kenai.jffi.Invoker",
   "methods":[
     {"name":"invokeI6","parameterTypes":["com.kenai.jffi.CallContext","long","int","int","int","int","int","int"] }, 
     {"name":"invokeL6","parameterTypes":["com.kenai.jffi.CallContext","long","long","long","long","long","long","long"] }, 
     {"name":"invokeN6","parameterTypes":["com.kenai.jffi.CallContext","long","long","long","long","long","long","long"] }
-  ]
-},
+  ]}
+,
 {
   "name":"com.kenai.jffi.Version",
   "fields":[
     {"name":"MAJOR"}, 
     {"name":"MICRO"}, 
     {"name":"MINOR"}
-  ]
-},
+  ]}
+,
 {
   "name":"com.kenai.jffi.internal.StubLoader",
-  "methods":[{"name":"isLoaded","parameterTypes":[] }]
-},
+  "methods":[{"name":"isLoaded","parameterTypes":[] }]}
+,
 {
   "name":"com.sun.crypto.provider.AESCipher$General",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
+{
+  "name":"com.sun.crypto.provider.ARCFOURCipher",
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
+{
+  "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305",
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
+{
+  "name":"com.sun.crypto.provider.DESCipher",
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
+{
+  "name":"com.sun.crypto.provider.DESedeCipher",
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"com.sun.crypto.provider.DHParameters",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
+{
+  "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM",
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"com.sun.crypto.provider.HmacCore$HmacSHA256",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
+{
+  "name":"com.sun.crypto.provider.HmacCore$HmacSHA384",
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"com.sun.crypto.provider.TlsKeyMaterialGenerator",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"com.sun.crypto.provider.TlsMasterSecretGenerator",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"com.sun.crypto.provider.TlsPrfGenerator$V12",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"int",
   "allDeclaredMethods":true,
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"int[]",
   "allDeclaredMethods":true,
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"java.io.Serializable",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.lang.Boolean",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
+{
+  "name":"java.lang.Class",
+  "methods":[{"name":"getRecordComponents","parameterTypes":[] }]}
+,
 {
   "name":"java.lang.Comparable",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.lang.Double",
-  "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
-},
+  "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]}
+,
 {
   "name":"java.lang.Enum",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.lang.Integer",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true,
-  "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"java.lang.Iterable",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.lang.Long",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true,
-  "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"java.lang.Number",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
+{
+  "name":"java.lang.Record",
+  "allDeclaredFields":true,
+  "queryAllDeclaredMethods":true}
+,
 {
   "name":"java.lang.String",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"java.lang.reflect.Method",
-  "methods":[{"name":"isDefault","parameterTypes":[] }]
-},
+  "methods":[{"name":"isDefault","parameterTypes":[] }]}
+,
+{
+  "name":"java.lang.reflect.RecordComponent",
+  "methods":[
+    {"name":"getName","parameterTypes":[] }, 
+    {"name":"getType","parameterTypes":[] }
+  ]}
+,
 {
   "name":"java.nio.Buffer",
   "allDeclaredMethods":true,
-  "fields":[{"name":"address"}]
-},
+  "fields":[{"name":"address"}]}
+,
 {
   "name":"java.nio.ByteBuffer",
   "allDeclaredMethods":true,
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
-  "name":"java.security.KeyStoreSpi"
-},
+  "name":"java.security.KeyStoreSpi"}
+,
 {
-  "name":"java.security.SecureRandomParameters"
-},
+  "name":"java.security.SecureRandomParameters"}
+,
 {
-  "name":"java.security.cert.PKIXRevocationChecker"
-},
+  "name":"java.security.cert.PKIXRevocationChecker"}
+,
 {
-  "name":"java.security.interfaces.ECPrivateKey"
-},
+  "name":"java.security.interfaces.ECPrivateKey"}
+,
 {
-  "name":"java.security.interfaces.ECPublicKey"
-},
+  "name":"java.security.interfaces.ECPublicKey"}
+,
 {
-  "name":"java.security.interfaces.RSAPrivateKey"
-},
+  "name":"java.security.interfaces.RSAPrivateKey"}
+,
 {
-  "name":"java.security.interfaces.RSAPublicKey"
-},
+  "name":"java.security.interfaces.RSAPublicKey"}
+,
 {
   "name":"java.util.AbstractCollection",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.util.AbstractList",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.util.ArrayList",
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"java.util.Collection",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.util.HashSet",
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"java.util.LinkedHashMap",
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"java.util.List",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.util.Locale",
-  "methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }]
-},
+  "methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }]}
+,
 {
   "name":"java.util.RandomAccess",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"java.util.UUID",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
+{
+  "name":"javax.security.auth.x500.X500Principal",
+  "methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }]}
+,
 {
-  "name":"jnr.constants.platform.linux.ProtocolFamily"
-},
+  "name":"jnr.constants.platform.linux.ProtocolFamily"}
+,
 {
-  "name":"jnr.constants.platform.linux.Shutdown"
-},
+  "name":"jnr.constants.platform.linux.Shutdown"}
+,
 {
-  "name":"jnr.constants.platform.linux.Sock"
-},
+  "name":"jnr.constants.platform.linux.Sock"}
+,
 {
-  "name":"jnr.constants.platform.linux.SocketLevel"
-},
+  "name":"jnr.constants.platform.linux.SocketLevel"}
+,
 {
-  "name":"jnr.constants.platform.linux.SocketOption"
-},
+  "name":"jnr.constants.platform.linux.SocketOption"}
+,
 {
   "name":"jnr.enxio.channels.Native$LibC",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"jnr.enxio.channels.Native$LibC$jnr$ffi$1",
-  "methods":[{"name":"<init>","parameterTypes":["jnr.ffi.Runtime","jnr.ffi.provider.jffi.NativeLibrary","java.lang.Object[]"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["jnr.ffi.Runtime","jnr.ffi.provider.jffi.NativeLibrary","java.lang.Object[]"] }]}
+,
 {
   "name":"jnr.ffi.Pointer",
   "allDeclaredMethods":true,
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"jnr.ffi.StructLayout$gid_t",
-  "methods":[{"name":"<init>","parameterTypes":["jnr.ffi.StructLayout"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["jnr.ffi.StructLayout"] }]}
+,
 {
   "name":"jnr.ffi.byref.IntByReference",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"jnr.ffi.provider.converters.ByReferenceParameterConverter",
-  "methods":[{"name":"nativeType","parameterTypes":[] }]
-},
+  "methods":[{"name":"nativeType","parameterTypes":[] }]}
+,
 {
   "name":"jnr.ffi.provider.converters.ByReferenceParameterConverter$Out",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"jnr.ffi.provider.converters.StringResultConverter",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"jnr.ffi.provider.converters.StructByReferenceToNativeConverter",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"jnr.ffi.provider.jffi.BufferParameterStrategy",
-  "methods":[{"name":"address","parameterTypes":["java.nio.Buffer"] }]
-},
+  "methods":[{"name":"address","parameterTypes":["java.nio.Buffer"] }]}
+,
 {
   "name":"jnr.ffi.provider.jffi.PointerParameterStrategy",
-  "methods":[{"name":"address","parameterTypes":["jnr.ffi.Pointer"] }]
-},
+  "methods":[{"name":"address","parameterTypes":["jnr.ffi.Pointer"] }]}
+,
 {
   "name":"jnr.ffi.provider.jffi.Provider",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"jnr.ffi.provider.jffi.platform.x86_64.linux.TypeAliases",
-  "fields":[{"name":"ALIASES"}]
-},
+  "fields":[{"name":"ALIASES"}]}
+,
 {
   "name":"jnr.posix.Timeval",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"jnr.unixsocket.Native$LibC",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"jnr.unixsocket.Native$LibC$jnr$ffi$0",
-  "methods":[{"name":"<init>","parameterTypes":["jnr.ffi.Runtime","jnr.ffi.provider.jffi.NativeLibrary","java.lang.Object[]"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["jnr.ffi.Runtime","jnr.ffi.provider.jffi.NativeLibrary","java.lang.Object[]"] }]}
+,
 {
   "name":"jnr.unixsocket.SockAddrUnix",
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
   "name":"long",
   "allDeclaredMethods":true,
-  "allPublicMethods":true
-},
+  "allPublicMethods":true}
+,
 {
-  "name":"long[]"
-},
+  "name":"long[]"}
+,
 {
   "name":"org.asamk.Signal",
   "allDeclaredMethods":true,
-  "allDeclaredClasses":true
-},
+  "allDeclaredClasses":true}
+,
+{
+  "name":"org.asamk.Signal$Device",
+  "allDeclaredMethods":true,
+  "allDeclaredClasses":true}
+,
+{
+  "name":"org.asamk.Signal$Group",
+  "allDeclaredMethods":true,
+  "allDeclaredClasses":true}
+,
 {
   "name":"org.asamk.Signal$MessageReceived",
   "allDeclaredConstructors":true,
-  "allPublicConstructors":true
-},
+  "allPublicConstructors":true}
+,
 {
   "name":"org.asamk.Signal$ReceiptReceived",
   "allDeclaredConstructors":true,
-  "allPublicConstructors":true
-},
+  "allPublicConstructors":true}
+,
+{
+  "name":"org.asamk.Signal$StructDevice",
+  "allDeclaredFields":true}
+,
+{
+  "name":"org.asamk.Signal$StructGroup",
+  "allDeclaredFields":true}
+,
 {
   "name":"org.asamk.Signal$SyncMessageReceived",
   "allDeclaredConstructors":true,
-  "allPublicConstructors":true
-},
+  "allPublicConstructors":true}
+,
+{
+  "name":"org.asamk.SignalControl",
+  "allDeclaredMethods":true,
+  "allDeclaredClasses":true}
+,
 {
   "name":"org.asamk.signal.commands.GetUserStatusCommand$JsonUserStatus",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.commands.ListContactsCommand$JsonContact",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.commands.ListDevicesCommand$JsonDevice",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.commands.ListGroupsCommand$JsonGroup",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.commands.ListGroupsCommand$JsonGroupMember",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.commands.ListIdentitiesCommand$JsonIdentity",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonAttachment",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonCallMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonContactAddress",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonContactAvatar",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonContactEmail",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonContactName",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonContactPhone",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonDataMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonError",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonGroupInfo",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonMention",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonMessageEnvelope",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonQuote",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonQuotedAttachment",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonReaction",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonReceiptMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonRemoteDelete",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonSharedContact",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonSticker",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonSyncDataMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonSyncMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonSyncMessageType",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.asamk.signal.json.JsonSyncReadMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.json.JsonTypingMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.jsonrpc.JsonRpcBulkMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.jsonrpc.JsonRpcException",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.jsonrpc.JsonRpcMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.jsonrpc.JsonRpcRequest",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.jsonrpc.JsonRpcResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.jsonrpc.JsonRpcResponse$Error",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
   "allDeclaredConstructors":true,
-  "fields":[{"name":"contacts", "allowWrite":true}]
-},
+  "fields":[{"name":"contacts", "allowWrite":true}]}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupInfo",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupInfoV1",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupStore$GroupsDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$Group",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$GroupV1",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$GroupV1$MembersDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$GroupV1$MembersSerializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$GroupV2",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.identities.IdentityKeyStore$IdentityStorage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
   "allDeclaredConstructors":true,
-  "fields":[{"name":"profiles", "allowWrite":true}]
-},
+  "fields":[{"name":"profiles", "allowWrite":true}]}
+,
 {
   "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.profiles.ProfileStore",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.profiles.SignalProfile",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.profiles.SignalProfile$Capabilities",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonIdentityKeyStore$JsonIdentityKeyStoreDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonPreKeyStore$JsonPreKeyStoreDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonSessionStore$JsonSessionStoreDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonSignedPreKeyStore$JsonSignedPreKeyStoreDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
   "allDeclaredConstructors":true,
-  "fields":[{"name":"addresses", "allowWrite":true}]
-},
+  "fields":[{"name":"addresses", "allowWrite":true}]}
+,
 {
   "name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore$RecipientStoreDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient$Contact",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient$Profile",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage$SharedSenderKey",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.stickers.StickerStore",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
   "allDeclaredConstructors":true,
-  "fields":[{"name":"stickers", "allowWrite":true}]
-},
+  "fields":[{"name":"stickers", "allowWrite":true}]}
+,
 {
   "name":"org.asamk.signal.manager.storage.stickers.StickerStore$Storage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.manager.storage.stickers.StickerStore$Storage$Sticker",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.asamk.signal.util.SecurityProvider$DefaultRandom",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.DSA$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.DSTU4145$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.EC$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.ECGOST$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.EdEC$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.ElGamal$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.GM$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.GOST$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.IES$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.X509$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.edec.SignatureSpi$Ed25519",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.edec.SignatureSpi$Ed448",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.Blake2b$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.Blake2s$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.DSTU7564$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.GOST3411$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.Haraka$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.Keccak$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.MD2$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.MD4$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.MD5$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD128$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD160$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD256$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD320$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.SHA1$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.SHA224$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.SHA256$Digest",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.SHA256$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.SHA3$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.SHA384$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.SHA512$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.SM3$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.Skein$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.Tiger$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.digest.Whirlpool$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.drbg.DRBG$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.keystore.BC$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.keystore.BCFKS$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.keystore.PKCS12$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Std",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.AES$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.ARC4$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.ARIA$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Blowfish$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.CAST5$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.CAST6$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Camellia$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.ChaCha$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.DES$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.DESede$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.DSTU7624$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.GOST28147$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.GOST3412_2015$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Grain128$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Grainv1$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.HC128$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.HC256$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.IDEA$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Noekeon$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.OpenSSLPBKDF$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF1$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF2$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPKCS12$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Poly1305$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.RC2$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.RC5$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.RC6$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Rijndael$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.SCRYPT$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.SEED$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.SM4$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Salsa20$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Serpent$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Shacal2$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash128$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Skipjack$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.TEA$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.TLSKDF$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Threefish$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Twofish$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.VMPC$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.VMPCKSA3$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.XSalsa20$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.XTEA$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.bouncycastle.jcajce.provider.symmetric.Zuc$Mappings",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.freedesktop.dbus.errors.ServiceUnknown",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
+,
+{
+  "name":"org.freedesktop.dbus.errors.UnknownObject",
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
+,
 {
   "name":"org.freedesktop.dbus.interfaces.DBus$NameAcquired",
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.freedesktop.dbus.interfaces.Introspectable",
   "allDeclaredMethods":true,
-  "allDeclaredClasses":true
-},
+  "allDeclaredClasses":true}
+,
 {
   "name":"org.freedesktop.dbus.interfaces.Peer",
   "allDeclaredMethods":true,
-  "allDeclaredClasses":true
-},
+  "allDeclaredClasses":true}
+,
+{
+  "name":"org.freedesktop.dbus.interfaces.Properties",
+  "allDeclaredMethods":true,
+  "allDeclaredClasses":true}
+,
+{
+  "name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged",
+  "allPublicConstructors":true}
+,
 {
-  "name":"org.objectweb.asm.util.TraceMethodVisitor"
-},
+  "name":"org.objectweb.asm.util.TraceMethodVisitor"}
+,
 {
   "name":"org.signal.storageservice.protos.groups.AccessControl",
   "fields":[
     {"name":"addFromInviteLink_"}, 
     {"name":"attributes_"}, 
     {"name":"members_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.AvatarUploadAttributes",
   "fields":[
     {"name":"key_"}, 
     {"name":"policy_"}, 
     {"name":"signature_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.Group",
   "fields":[
     {"name":"requestingMembers_"}, 
     {"name":"revision_"}, 
     {"name":"title_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupAttributeBlob",
   "fields":[
     {"name":"contentCase_"}, 
     {"name":"content_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange",
   "fields":[
     {"name":"actions_"}, 
     {"name":"changeEpoch_"}, 
     {"name":"serverSignature_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions",
   "fields":[
     {"name":"promoteRequestingMembers_"}, 
     {"name":"revision_"}, 
     {"name":"sourceUuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddMemberAction",
   "fields":[
     {"name":"added_"}, 
     {"name":"joinFromInviteLink_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddPendingMemberAction",
-  "fields":[{"name":"added_"}]
-},
+  "fields":[{"name":"added_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddRequestingMemberAction",
-  "fields":[{"name":"added_"}]
-},
+  "fields":[{"name":"added_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeleteMemberAction",
-  "fields":[{"name":"deletedUserId_"}]
-},
+  "fields":[{"name":"deletedUserId_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeletePendingMemberAction",
-  "fields":[{"name":"deletedUserId_"}]
-},
+  "fields":[{"name":"deletedUserId_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeleteRequestingMemberAction",
-  "fields":[{"name":"deletedUserId_"}]
-},
+  "fields":[{"name":"deletedUserId_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyAddFromInviteLinkAccessControlAction",
-  "fields":[{"name":"addFromInviteLinkAccess_"}]
-},
+  "fields":[{"name":"addFromInviteLinkAccess_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyAttributesAccessControlAction",
-  "fields":[{"name":"attributesAccess_"}]
-},
+  "fields":[{"name":"attributesAccess_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyDescriptionAction",
-  "fields":[{"name":"description_"}]
-},
+  "fields":[{"name":"description_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyDisappearingMessagesTimerAction",
-  "fields":[{"name":"timer_"}]
-},
+  "fields":[{"name":"timer_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyInviteLinkPasswordAction",
-  "fields":[{"name":"inviteLinkPassword_"}]
-},
+  "fields":[{"name":"inviteLinkPassword_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMemberProfileKeyAction",
-  "fields":[{"name":"presentation_"}]
-},
+  "fields":[{"name":"presentation_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMemberRoleAction",
   "fields":[
     {"name":"role_"}, 
     {"name":"userId_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMembersAccessControlAction",
-  "fields":[{"name":"membersAccess_"}]
-},
+  "fields":[{"name":"membersAccess_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyTitleAction",
-  "fields":[{"name":"title_"}]
-},
+  "fields":[{"name":"title_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$PromotePendingMemberAction",
-  "fields":[{"name":"presentation_"}]
-},
+  "fields":[{"name":"presentation_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$PromoteRequestingMemberAction",
   "fields":[
     {"name":"role_"}, 
     {"name":"userId_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupInviteLink",
   "fields":[
     {"name":"contentsCase_"}, 
     {"name":"contents_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.GroupInviteLink$GroupInviteLinkContentsV1",
   "fields":[
     {"name":"groupMasterKey_"}, 
     {"name":"inviteLinkPassword_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.Member",
   "fields":[
     {"name":"profileKey_"}, 
     {"name":"role_"}, 
     {"name":"userId_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.PendingMember",
   "fields":[
     {"name":"addedByUserId_"}, 
     {"name":"member_"}, 
     {"name":"timestamp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.RequestingMember",
   "fields":[
     {"name":"profileKey_"}, 
     {"name":"timestamp_"}, 
     {"name":"userId_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedGroup",
   "fields":[
     {"name":"requestingMembers_"}, 
     {"name":"revision_"}, 
     {"name":"title_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedGroupChange",
   "fields":[
     {"name":"promotePendingMembers_"}, 
     {"name":"promoteRequestingMembers_"}, 
     {"name":"revision_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedMember",
   "fields":[
     {"name":"profileKey_"}, 
     {"name":"role_"}, 
     {"name":"uuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole",
   "fields":[
     {"name":"role_"}, 
     {"name":"uuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedPendingMember",
   "fields":[
     {"name":"timestamp_"}, 
     {"name":"uuidCipherText_"}, 
     {"name":"uuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval",
   "fields":[
     {"name":"uuidCipherText_"}, 
     {"name":"uuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedRequestingMember",
   "fields":[
     {"name":"profileKey_"}, 
     {"name":"timestamp_"}, 
     {"name":"uuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedString",
-  "fields":[{"name":"value_"}]
-},
+  "fields":[{"name":"value_"}]}
+,
 {
   "name":"org.signal.storageservice.protos.groups.local.DecryptedTimer",
-  "fields":[{"name":"duration_"}]
-},
+  "fields":[{"name":"duration_"}]}
+,
 {
   "name":"org.whispersystems.libsignal.state.IdentityKeyStore",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.whispersystems.libsignal.state.PreKeyStore",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.whispersystems.libsignal.state.SessionStore",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.whispersystems.libsignal.state.SignalProtocolStore",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.whispersystems.libsignal.state.SignedPreKeyStore",
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.account.AccountAttributes",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.account.AccountAttributes$Capabilities",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.groupsv2.CredentialResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
-  "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"
-},
+  "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"}
+,
 {
   "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage$Type",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.messages.calls.OfferMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.messages.calls.OfferMessage$Type",
   "allDeclaredFields":true,
-  "allDeclaredMethods":true
-},
+  "allDeclaredMethods":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfile",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
+{
+  "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfile$Badge",
+  "allDeclaredFields":true,
+  "allDeclaredMethods":true,
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfile$Capabilities",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.push.SignedPreKeyEntity",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.api.push.SignedPreKeyEntity$ByteArrayDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.signalservice.api.push.SignedPreKeyEntity$ByteArraySerializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.crypto.SignatureBodyEntity",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.MultiRemoteAttestationResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.QueryEnvelope",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.contacts.entities.TokenResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.devices.DeviceNameProtos$DeviceName",
   "fields":[
     {"name":"ciphertext_"}, 
     {"name":"ephemeralPublic_"}, 
     {"name":"syntheticIv_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.keybackup.protos.BackupRequest",
   "fields":[
     {"name":"token_"}, 
     {"name":"tries_"}, 
     {"name":"validFrom_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"status_"}, 
     {"name":"token_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.keybackup.protos.DeleteRequest",
   "fields":[
     {"name":"backupId_"}, 
     {"name":"bitField0_"}, 
     {"name":"serviceId_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.keybackup.protos.Request",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"delete_"}, 
     {"name":"restore_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.keybackup.protos.Response",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"delete_"}, 
     {"name":"restore_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreRequest",
   "fields":[
     {"name":"serviceId_"}, 
     {"name":"token_"}, 
     {"name":"validFrom_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse",
   "fields":[
     {"name":"status_"}, 
     {"name":"token_"}, 
     {"name":"tries_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.AuthCredentials",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.ConfirmCodeMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.DeviceCode",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.DeviceId",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.DeviceInfoList",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.MismatchedDevices",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.OutgoingPushMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.OutgoingPushMessageList",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PreKeyEntity",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PreKeyEntity$ECPublicKeyDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PreKeyEntity$ECPublicKeySerializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PreKeyResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PreKeyResponseItem",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PreKeyState",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PreKeyStatus",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.ProfileAvatarUploadAttributes",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.ProvisioningMessage",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisionEnvelope",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"body_"}, 
     {"name":"publicKey_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisionMessage",
   "fields":[
     {"name":"readReceipts_"}, 
     {"name":"userAgent_"}, 
     {"name":"uuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisioningUuid",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"uuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PushServiceSocket$RegistrationLockFailure",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.PushServiceSocket$RegistrationLockV2",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SendMessageResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SenderCertificate",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SenderCertificate$ByteArrayDesieralizer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$AttachmentPointer",
   "fields":[
     {"name":"thumbnail_"}, 
     {"name":"uploadTimestamp_"}, 
     {"name":"width_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage",
   "fields":[
     {"name":"multiRing_"}, 
     {"name":"offer_"}, 
     {"name":"opaque_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Hangup",
   "fields":[
     {"name":"deviceId_"}, 
     {"name":"id_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$IceUpdate",
   "fields":[
     {"name":"mid_"}, 
     {"name":"opaque_"}, 
     {"name":"sdp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Offer",
   "fields":[
     {"name":"opaque_"}, 
     {"name":"sdp_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Opaque",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"data_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ContactDetails",
   "fields":[
     {"name":"profileKey_"}, 
     {"name":"uuid_"}, 
     {"name":"verified_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ContactDetails$Avatar",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"contentType_"}, 
     {"name":"length_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Content",
   "fields":[
     {"name":"senderKeyDistributionMessage_"}, 
     {"name":"syncMessage_"}, 
     {"name":"typingMessage_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage",
   "fields":[
     {"name":"requiredProtocolVersion_"}, 
     {"name":"sticker_"}, 
     {"name":"timestamp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$BodyRange",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"length_"}, 
     {"name":"start_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact",
   "fields":[
     {"name":"name_"}, 
     {"name":"number_"}, 
     {"name":"organization_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Avatar",
   "fields":[
     {"name":"avatar_"}, 
     {"name":"bitField0_"}, 
     {"name":"isProfile_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Email",
   "fields":[
     {"name":"label_"}, 
     {"name":"type_"}, 
     {"name":"value_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Name",
   "fields":[
     {"name":"middleName_"}, 
     {"name":"prefix_"}, 
     {"name":"suffix_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Phone",
   "fields":[
     {"name":"label_"}, 
     {"name":"type_"}, 
     {"name":"value_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$PostalAddress",
   "fields":[
     {"name":"region_"}, 
     {"name":"street_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Delete",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"targetSentTimestamp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$GroupCallUpdate",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"eraId_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Preview",
   "fields":[
     {"name":"image_"}, 
     {"name":"title_"}, 
     {"name":"url_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Quote",
   "fields":[
     {"name":"bodyRanges_"}, 
     {"name":"id_"}, 
     {"name":"text_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Reaction",
   "fields":[
     {"name":"remove_"}, 
     {"name":"targetAuthorUuid_"}, 
     {"name":"targetSentTimestamp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Sticker",
   "fields":[
     {"name":"packId_"}, 
     {"name":"packKey_"}, 
     {"name":"stickerId_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Envelope",
   "fields":[
     {"name":"sourceUuid_"}, 
     {"name":"timestamp_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$GroupContext",
   "fields":[
     {"name":"members_"}, 
     {"name":"name_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$GroupContext$Member",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"e164_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$GroupContextV2",
   "fields":[
     {"name":"groupChange_"}, 
     {"name":"masterKey_"}, 
     {"name":"revision_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$NullMessage",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"padding_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ReceiptMessage",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"timestamp_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage",
   "fields":[
     {"name":"verified_"}, 
     {"name":"viewOnceOpen_"}, 
     {"name":"viewed_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Blocked",
   "fields":[
     {"name":"groupIds_"}, 
     {"name":"numbers_"}, 
     {"name":"uuids_"}
-  ]
-},
+  ]}
+,
+{
+  "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Configuration",
+  "fields":[
+    {"name":"bitField0_"}, 
+    {"name":"linkPreviews_"}, 
+    {"name":"provisioningVersion_"}, 
+    {"name":"readReceipts_"}, 
+    {"name":"typingIndicators_"}, 
+    {"name":"unidentifiedDeliveryIndicators_"}
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Contacts",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"blob_"}, 
     {"name":"complete_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$FetchLatest",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Keys",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"storageService_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Read",
   "fields":[
     {"name":"senderE164_"}, 
     {"name":"senderUuid_"}, 
     {"name":"timestamp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Request",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Sent",
   "fields":[
     {"name":"message_"}, 
     {"name":"timestamp_"}, 
     {"name":"unidentifiedStatus_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Sent$UnidentifiedDeliveryStatus",
   "fields":[
     {"name":"destinationE164_"}, 
     {"name":"destinationUuid_"}, 
     {"name":"unidentified_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$StickerPackOperation",
   "fields":[
     {"name":"packId_"}, 
     {"name":"packKey_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Viewed",
   "fields":[
     {"name":"senderE164_"}, 
     {"name":"senderUuid_"}, 
     {"name":"timestamp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$TypingMessage",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"groupId_"}, 
     {"name":"timestamp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Verified",
   "fields":[
     {"name":"identityKey_"}, 
     {"name":"nullMessage_"}, 
     {"name":"state_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.push.VerifyAccountResponse",
   "allDeclaredFields":true,
   "allDeclaredMethods":true,
-  "allDeclaredConstructors":true
-},
+  "allDeclaredConstructors":true}
+,
 {
   "name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto",
   "fields":[
     {"name":"bitField0_"}, 
     {"name":"e164_"}, 
     {"name":"uuid_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.serialize.protos.MetadataProto",
   "fields":[
     {"name":"serverGuid_"}, 
     {"name":"serverReceivedTimestamp_"}, 
     {"name":"timestamp_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto",
   "fields":[
     {"name":"data_"}, 
     {"name":"localAddress_"}, 
     {"name":"metadata_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.util.JsonUtil$IdentityKeyDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.util.JsonUtil$IdentityKeySerializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.util.JsonUtil$UuidDeserializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketMessage",
   "fields":[
     {"name":"request_"}, 
     {"name":"response_"}, 
     {"name":"type_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketRequestMessage",
   "fields":[
     {"name":"id_"}, 
     {"name":"path_"}, 
     {"name":"verb_"}
-  ]
-},
+  ]}
+,
 {
   "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketResponseMessage",
   "fields":[
     {"name":"id_"}, 
     {"name":"message_"}, 
     {"name":"status_"}
-  ]
-},
+  ]}
+,
 {
   "name":"sun.misc.Unsafe",
   "allDeclaredFields":true,
     {"name":"putLong","parameterTypes":["java.lang.Object","long","long"] }, 
     {"name":"putObject","parameterTypes":["java.lang.Object","long","java.lang.Object"] }, 
     {"name":"putShort","parameterTypes":["long","short"] }
-  ]
-},
+  ]}
+,
 {
   "name":"sun.security.provider.DSA$SHA224withDSA",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.JavaKeyStore$DualFormatJKS",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.JavaKeyStore$JKS",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.NativePRNG",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.SHA",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.SHA2$SHA224",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.SHA2$SHA256",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.SHA5$SHA384",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.SHA5$SHA512",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.SecureRandom",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.provider.certpath.PKIXCertPathValidator",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.rsa.PSSParameters",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.rsa.RSAKeyFactory$Legacy",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.rsa.RSAPSSSignature",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.rsa.RSASignature$SHA224withRSA",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.rsa.RSASignature$SHA256withRSA",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.rsa.RSASignature$SHA512withRSA",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.ssl.SSLContextImpl$TLSContext",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":[] }]}
+,
 {
   "name":"sun.security.x509.AuthorityKeyIdentifierExtension",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]}
+,
 {
   "name":"sun.security.x509.BasicConstraintsExtension",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]}
+,
 {
   "name":"sun.security.x509.CRLDistributionPointsExtension",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]}
+,
 {
   "name":"sun.security.x509.KeyUsageExtension",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]}
+,
 {
   "name":"sun.security.x509.SubjectAlternativeNameExtension",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
-},
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]}
+,
 {
   "name":"sun.security.x509.SubjectKeyIdentifierExtension",
-  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
-}
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]}
+
 ]
index 8153bb1cc6eb9793ecd440bfba395725d08eed0d..e6532ae8a130e00594a2a281d14c2306374f0ee0 100644 (file)
@@ -1,34 +1,92 @@
 {
   "resources":{
   "includes":[
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AT\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EE\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ES\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FI\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FR\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GB\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GR\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IN\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PL\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RU\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UA\\E"}, 
-    {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US\\E"}, 
-    {"pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\E"}, 
-    {"pattern":"\\Qjnr/constants/ConstantSet.class\\E"}, 
-    {"pattern":"\\Qjnr/constants/platform/linux/ProtocolFamily.class\\E"}, 
-    {"pattern":"\\Qjnr/constants/platform/linux/Shutdown.class\\E"}, 
-    {"pattern":"\\Qjnr/constants/platform/linux/Sock.class\\E"}, 
-    {"pattern":"\\Qjnr/constants/platform/linux/SocketLevel.class\\E"}, 
-    {"pattern":"\\Qjnr/constants/platform/linux/SocketOption.class\\E"}, 
-    {"pattern":"\\Qlibsignal_jni.so\\E"}, 
-    {"pattern":"\\Qlibzkgroup.so\\E"}, 
-    {"pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"}, 
-    {"pattern":"\\Qorg/asamk/signal/manager/config/whisper.store\\E"}, 
-    {"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"}, 
-    {"pattern":"com/google/i18n/phonenumbers/data/.*"}
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AT\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EE\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ES\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FI\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FR\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GB\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GR\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IN\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PL\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RU\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UA\\E"
+    }, 
+    {
+      "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US\\E"
+    }, 
+    {
+      "pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\E"
+    }, 
+    {
+      "pattern":"\\Qjnr/constants/ConstantSet.class\\E"
+    }, 
+    {
+      "pattern":"\\Qjnr/constants/platform/linux/ProtocolFamily.class\\E"
+    }, 
+    {
+      "pattern":"\\Qjnr/constants/platform/linux/Shutdown.class\\E"
+    }, 
+    {
+      "pattern":"\\Qjnr/constants/platform/linux/Sock.class\\E"
+    }, 
+    {
+      "pattern":"\\Qjnr/constants/platform/linux/SocketLevel.class\\E"
+    }, 
+    {
+      "pattern":"\\Qjnr/constants/platform/linux/SocketOption.class\\E"
+    }, 
+    {
+      "pattern":"\\Qlibsignal_jni.so\\E"
+    }, 
+    {
+      "pattern":"\\Qlibzkgroup.so\\E"
+    }, 
+    {
+      "pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"
+    }, 
+    {
+      "pattern":"\\Qorg/asamk/signal/manager/config/whisper.store\\E"
+    }, 
+    {
+      "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
+    }, 
+    {
+      "pattern":"com/google/i18n/phonenumbers/data/.*"
+    }
   ]},
-  "bundles":[{"name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl"}]
+  "bundles":[{
+      "name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl"
+    }]
 }
index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 (file)
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
index f371643eed77c0489147fbe387473065005d193a..ffed3a254e91df704a9acc0f2745c0e340d9b582 100644 (file)
@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 4f906e0c811fc9e230eb44819f509cd0627f2600..1b6c787337ffb79f0e3cf8b1e9f00f680a959de1 100755 (executable)
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
 
 #
-# Copyright 2015 the original author or authors.
+# Copyright Â© 2015-2021 the original authors.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 #
 
 ##############################################################################
-##
-##  Gradle start up script for UN*X
-##
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions Â«$var», Â«${var}», Â«${var:-default}», Â«${var+SET}»,
+#           Â«${var#prefix}», Â«${var%suffix}», and Â«$( cmd )»;
+#         * compound commands having a testable exit status, especially Â«case»;
+#         * various built-in commands including Â«command», Â«set», and Â«ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
 ##############################################################################
 
 # Attempt to set APP_HOME
+
 # Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
 done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
 
 APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+APP_BASE_NAME=${0##*/}
 
 # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
 
 warn () {
     echo "$*"
-}
+} >&2
 
 die () {
     echo
     echo "$*"
     echo
     exit 1
-}
+} >&2
 
 # OS specific support (must be 'true' or 'false').
 cygwin=false
 msys=false
 darwin=false
 nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
 esac
 
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 if [ -n "$JAVA_HOME" ] ; then
     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
         # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
+        JAVACMD=$JAVA_HOME/jre/sh/java
     else
-        JAVACMD="$JAVA_HOME/bin/java"
+        JAVACMD=$JAVA_HOME/bin/java
     fi
     if [ ! -x "$JAVACMD" ] ; then
         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
     fi
 else
-    JAVACMD="java"
+    JAVACMD=java
     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,95 @@ location of your Java installation."
 fi
 
 # Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
 fi
 
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
 
 # For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
     # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
         fi
-        i=`expr $i + 1`
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
     done
-    case $i in
-        0) set -- ;;
-        1) set -- "$args0" ;;
-        2) set -- "$args0" "$args1" ;;
-        3) set -- "$args0" "$args1" "$args2" ;;
-        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
 fi
 
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=`save "$@"`
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
 
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
 
 exec "$JAVACMD" "$@"
index 6e528805446ba9e6bcb6cbc327efe46249d569ba..c12ad0a46f0c7e6f0794a5f1aedc9071ff024792 100644 (file)
@@ -4,8 +4,8 @@ plugins {
 }
 
 java {
-    sourceCompatibility = JavaVersion.VERSION_11
-    targetCompatibility = JavaVersion.VERSION_11
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
 }
 
 repositories {
@@ -14,10 +14,11 @@ repositories {
 }
 
 dependencies {
-    api("com.github.turasa:signal-service-java:2.15.3_unofficial_28")
+    api("com.github.turasa:signal-service-java:2.15.3_unofficial_31")
+    api("com.fasterxml.jackson.core", "jackson-databind", "2.13.0")
     implementation("com.google.protobuf:protobuf-javalite:3.10.0")
     implementation("org.bouncycastle:bcprov-jdk15on:1.69")
-    implementation("org.slf4j:slf4j-api:1.7.30")
+    implementation("org.slf4j:slf4j-api:1.7.32")
 }
 
 configurations {
index 1f9d10ff897248f61c08d8f5c1023fef27544ff2..3e35acedcfd7927416f62b33446a30c709bfd5da 100644 (file)
@@ -15,10 +15,7 @@ import java.util.Map;
 
 import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
 
-public class DeviceLinkInfo {
-
-    final String deviceIdentifier;
-    final ECPublicKey deviceKey;
+public record DeviceLinkInfo(String deviceIdentifier, ECPublicKey deviceKey) {
 
     public static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws InvalidKeyException {
         final var rawQuery = linkUri.getRawQuery();
@@ -57,15 +54,10 @@ public class DeviceLinkInfo {
         return map;
     }
 
-    public DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) {
-        this.deviceIdentifier = deviceIdentifier;
-        this.deviceKey = deviceKey;
-    }
-
     public URI createDeviceLinkUri() {
         final var deviceKeyString = Base64.getEncoder().encodeToString(deviceKey.serialize()).replace("=", "");
         try {
-            return new URI("tsdevice:/?uuid="
+            return new URI("sgnl://linkdevice?uuid="
                     + URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8)
                     + "&pub_key="
                     + URLEncoder.encode(deviceKeyString, StandardCharsets.UTF_8));
index 74ee6d0231b32865a6cde3c7f3d4570d0b6ff5d6..891144264f8d3ec9b17a04f3d2052eadc8843ce7 100644 (file)
@@ -1,55 +1,8 @@
 package org.asamk.signal.manager;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.List;
 
-public class JsonStickerPack {
-
-    @JsonProperty
-    public String title;
-
-    @JsonProperty
-    public String author;
-
-    @JsonProperty
-    public JsonSticker cover;
-
-    @JsonProperty
-    public List<JsonSticker> stickers;
-
-    // For deserialization
-    private JsonStickerPack() {
-    }
-
-    public JsonStickerPack(
-            final String title, final String author, final JsonSticker cover, final List<JsonSticker> stickers
-    ) {
-        this.title = title;
-        this.author = author;
-        this.cover = cover;
-        this.stickers = stickers;
-    }
-
-    public static class JsonSticker {
-
-        @JsonProperty
-        public String emoji;
-
-        @JsonProperty
-        public String file;
-
-        @JsonProperty
-        public String contentType;
-
-        // For deserialization
-        private JsonSticker() {
-        }
+public record JsonStickerPack(String title, String author, JsonSticker cover, List<JsonSticker> stickers) {
 
-        public JsonSticker(final String emoji, final String file, final String contentType) {
-            this.emoji = emoji;
-            this.file = file;
-            this.contentType = contentType;
-        }
-    }
+    public record JsonSticker(String emoji, String file, String contentType) {}
 }
index 3be4d7e9617ad853e19d35c63b17aa12b12c284d..c14b0f13be92cb889d5c6941a22046bed20d2d66 100644 (file)
@@ -20,22 +20,11 @@ public class LibSignalLogger implements SignalProtocolLogger {
     public void log(final int priority, final String tag, final String message) {
         final var logMessage = String.format("[%s]: %s", tag, message);
         switch (priority) {
-            case SignalProtocolLogger.VERBOSE:
-                logger.trace(logMessage);
-                break;
-            case SignalProtocolLogger.DEBUG:
-                logger.debug(logMessage);
-                break;
-            case SignalProtocolLogger.INFO:
-                logger.info(logMessage);
-                break;
-            case SignalProtocolLogger.WARN:
-                logger.warn(logMessage);
-                break;
-            case SignalProtocolLogger.ERROR:
-            case SignalProtocolLogger.ASSERT:
-                logger.error(logMessage);
-                break;
+            case SignalProtocolLogger.VERBOSE -> logger.trace(logMessage);
+            case SignalProtocolLogger.DEBUG -> logger.debug(logMessage);
+            case SignalProtocolLogger.INFO -> logger.info(logMessage);
+            case SignalProtocolLogger.WARN -> logger.warn(logMessage);
+            case SignalProtocolLogger.ERROR, SignalProtocolLogger.ASSERT -> logger.error(logMessage);
         }
     }
 }
index 7a4219660dd959c6af3cc14af8e5fafe77b182e1..2cb68d206eaba52efbcbd188dcd28b4d50fd0dc3 100644 (file)
@@ -8,13 +8,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.api.SendGroupMessageResults;
 import org.asamk.signal.manager.api.SendMessageResults;
 import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.config.ServiceConfig;
 import org.asamk.signal.manager.config.ServiceEnvironment;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
-import org.asamk.signal.manager.groups.GroupLinkState;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
-import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
 import org.asamk.signal.manager.groups.LastGroupAdminException;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
@@ -23,7 +22,6 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
 import org.asamk.signal.manager.storage.recipients.Contact;
 import org.asamk.signal.manager.storage.recipients.Profile;
 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
-import org.whispersystems.libsignal.IdentityKey;
 import org.whispersystems.libsignal.InvalidKeyException;
 import org.whispersystems.libsignal.util.Pair;
 import org.whispersystems.libsignal.util.guava.Optional;
@@ -59,11 +57,11 @@ public interface Manager extends Closeable {
     ) throws IOException, NotRegisteredException {
         var pathConfig = PathConfig.createDefault(settingsPath);
 
-        if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) {
+        if (!SignalAccount.userExists(pathConfig.dataPath(), number)) {
             throw new NotRegisteredException();
         }
 
-        var account = SignalAccount.load(pathConfig.getDataPath(), number, true, trustNewIdentity);
+        var account = SignalAccount.load(pathConfig.dataPath(), number, true, trustNewIdentity);
 
         if (!account.isRegistered()) {
             throw new NotRegisteredException();
@@ -76,7 +74,7 @@ public interface Manager extends Closeable {
 
     static List<String> getAllLocalNumbers(File settingsPath) {
         var pathConfig = PathConfig.createDefault(settingsPath);
-        final var dataPath = pathConfig.getDataPath();
+        final var dataPath = pathConfig.dataPath();
         final var files = dataPath.listFiles();
 
         if (files == null) {
@@ -138,20 +136,7 @@ public interface Manager extends Closeable {
     ) throws IOException, AttachmentInvalidException;
 
     SendGroupMessageResults updateGroup(
-            GroupId groupId,
-            String name,
-            String description,
-            Set<RecipientIdentifier.Single> members,
-            Set<RecipientIdentifier.Single> removeMembers,
-            Set<RecipientIdentifier.Single> admins,
-            Set<RecipientIdentifier.Single> 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<GroupId, SendGroupMessageResults> joinGroup(
@@ -198,7 +183,7 @@ public interface Manager extends Closeable {
 
     void setGroupBlocked(
             GroupId groupId, boolean blocked
-    ) throws GroupNotFoundException, IOException;
+    ) throws GroupNotFoundException, IOException, NotMasterDeviceException;
 
     void setExpirationTimer(
             RecipientIdentifier.Single recipient, int messageExpirationTimer
@@ -208,13 +193,31 @@ public interface Manager extends Closeable {
 
     void requestAllSyncData() throws IOException;
 
-    void receiveMessages(
-            long timeout,
-            TimeUnit unit,
-            boolean returnOnTimeout,
-            boolean ignoreAttachments,
-            ReceiveMessageHandler handler
-    ) throws IOException;
+    /**
+     * Add a handler to receive new messages.
+     * Will start receiving messages from server, if not already started.
+     */
+    void addReceiveHandler(ReceiveMessageHandler handler);
+
+    /**
+     * Remove a handler to receive new messages.
+     * Will stop receiving messages from server, if this was the last registered receiver.
+     */
+    void removeReceiveHandler(ReceiveMessageHandler handler);
+
+    boolean isReceiving();
+
+    /**
+     * Receive new messages from server, returns if no new message arrive in a timespan of timeout.
+     */
+    void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException;
+
+    /**
+     * Receive new messages from server, returns only if the thread is interrupted.
+     */
+    void receiveMessages(ReceiveMessageHandler handler) throws IOException;
+
+    void setIgnoreAttachments(boolean ignoreAttachments);
 
     boolean hasCaughtUpWithOldMessages();
 
@@ -242,8 +245,6 @@ public interface Manager extends Closeable {
 
     boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient);
 
-    String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey);
-
     SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address);
 
     @Override
index 0fd1eb33cf0d69a937f45ab6e52c4d0a91b2ccb8..fe26e9b4d340a7c1115371961bf0d7ea1da64e30 100644 (file)
@@ -25,13 +25,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.api.SendGroupMessageResults;
 import org.asamk.signal.manager.api.SendMessageResults;
 import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.config.ServiceConfig;
 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
-import org.asamk.signal.manager.groups.GroupLinkState;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
-import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
 import org.asamk.signal.manager.groups.LastGroupAdminException;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
@@ -39,6 +38,7 @@ import org.asamk.signal.manager.helper.AttachmentHelper;
 import org.asamk.signal.manager.helper.ContactHelper;
 import org.asamk.signal.manager.helper.GroupHelper;
 import org.asamk.signal.manager.helper.GroupV2Helper;
+import org.asamk.signal.manager.helper.IdentityHelper;
 import org.asamk.signal.manager.helper.IncomingMessageHandler;
 import org.asamk.signal.manager.helper.PinHelper;
 import org.asamk.signal.manager.helper.PreKeyHelper;
@@ -60,15 +60,10 @@ import org.asamk.signal.manager.storage.stickers.Sticker;
 import org.asamk.signal.manager.storage.stickers.StickerPackId;
 import org.asamk.signal.manager.util.KeyUtils;
 import org.asamk.signal.manager.util.StickerUtils;
-import org.asamk.signal.manager.util.Utils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.whispersystems.libsignal.IdentityKey;
 import org.whispersystems.libsignal.InvalidKeyException;
 import org.whispersystems.libsignal.ecc.ECPublicKey;
-import org.whispersystems.libsignal.fingerprint.Fingerprint;
-import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
-import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
 import org.whispersystems.libsignal.util.Pair;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.SignalSessionLock;
@@ -99,9 +94,7 @@ import java.net.URISyntaxException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.security.SignatureException;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -113,7 +106,6 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.locks.ReentrantLock;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
@@ -139,9 +131,15 @@ public class ManagerImpl implements Manager {
     private final ContactHelper contactHelper;
     private final IncomingMessageHandler incomingMessageHandler;
     private final PreKeyHelper preKeyHelper;
+    private final IdentityHelper identityHelper;
 
     private final Context context;
     private boolean hasCaughtUpWithOldMessages = false;
+    private boolean ignoreAttachments = false;
+
+    private Thread receiveThread;
+    private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
+    private boolean isReceivingSynchronous;
 
     ManagerImpl(
             SignalAccount account,
@@ -171,20 +169,19 @@ public class ManagerImpl implements Manager {
                 account.getSignalProtocolStore(),
                 executor,
                 sessionLock);
-        final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
-        final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
-        final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
+        final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
+        final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath());
+        final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath());
 
         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 +237,11 @@ public class ManagerImpl implements Manager {
                 syncHelper,
                 this::getRecipientProfile,
                 jobExecutor);
+        this.identityHelper = new IdentityHelper(account,
+                dependencies,
+                this::resolveSignalServiceAddress,
+                syncHelper,
+                profileHelper);
     }
 
     @Override
@@ -424,7 +426,7 @@ public class ManagerImpl implements Manager {
     public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
         var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
 
-        addDevice(info.deviceIdentifier, info.deviceKey);
+        addDevice(info.deviceIdentifier(), info.deviceKey());
     }
 
     private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
@@ -505,9 +507,12 @@ public class ManagerImpl implements Manager {
                         .map(account.getRecipientStore()::resolveRecipientAddress)
                         .collect(Collectors.toSet()),
                 groupInfo.isBlocked(),
-                groupInfo.getMessageExpirationTime(),
-                groupInfo.isAnnouncementGroup(),
-                groupInfo.isMember(account.getSelfRecipientId()));
+                groupInfo.getMessageExpirationTimer(),
+                groupInfo.getPermissionAddMember(),
+                groupInfo.getPermissionEditDetails(),
+                groupInfo.getPermissionSendMessage(),
+                groupInfo.isMember(account.getSelfRecipientId()),
+                groupInfo.isAdmin(account.getSelfRecipientId()));
     }
 
     @Override
@@ -532,35 +537,22 @@ public class ManagerImpl implements Manager {
 
     @Override
     public SendGroupMessageResults updateGroup(
-            GroupId groupId,
-            String name,
-            String description,
-            Set<RecipientIdentifier.Single> members,
-            Set<RecipientIdentifier.Single> removeMembers,
-            Set<RecipientIdentifier.Single> admins,
-            Set<RecipientIdentifier.Single> 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
@@ -577,16 +569,15 @@ public class ManagerImpl implements Manager {
         long timestamp = System.currentTimeMillis();
         messageBuilder.withTimestamp(timestamp);
         for (final var recipient : recipients) {
-            if (recipient instanceof RecipientIdentifier.Single) {
-                final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient);
+            if (recipient instanceof RecipientIdentifier.Single single) {
+                final var recipientId = resolveRecipient(single);
                 final var result = sendHelper.sendMessage(messageBuilder, recipientId);
                 results.put(recipient, List.of(result));
             } else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
                 final var result = sendHelper.sendSelfMessage(messageBuilder);
                 results.put(recipient, List.of(result));
-            } else if (recipient instanceof RecipientIdentifier.Group) {
-                final var groupId = ((RecipientIdentifier.Group) recipient).groupId;
-                final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId);
+            } else if (recipient instanceof RecipientIdentifier.Group group) {
+                final var result = sendHelper.sendAsGroupMessage(messageBuilder, group.groupId);
                 results.put(recipient, result);
             }
         }
@@ -651,8 +642,8 @@ public class ManagerImpl implements Manager {
     private void applyMessage(
             final SignalServiceDataMessage.Builder messageBuilder, final Message message
     ) throws AttachmentInvalidException, IOException {
-        messageBuilder.withBody(message.getMessageText());
-        final var attachments = message.getAttachments();
+        messageBuilder.withBody(message.messageText());
+        final var attachments = message.attachments();
         if (attachments != null) {
             messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments));
         }
@@ -726,7 +717,10 @@ public class ManagerImpl implements Manager {
     @Override
     public void setGroupBlocked(
             final GroupId groupId, final boolean blocked
-    ) throws GroupNotFoundException, IOException {
+    ) throws GroupNotFoundException, IOException, NotMasterDeviceException {
+        if (!account.isMasterDevice()) {
+            throw new NotMasterDeviceException();
+        }
         groupHelper.setGroupBlocked(groupId, blocked);
         // TODO cycle our profile key
         syncHelper.sendBlockedList();
@@ -793,22 +787,6 @@ public class ManagerImpl implements Manager {
         }
     }
 
-    private byte[] getSenderCertificate() {
-        byte[] certificate;
-        try {
-            if (account.isPhoneNumberShared()) {
-                certificate = dependencies.getAccountManager().getSenderCertificate();
-            } else {
-                certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
-            }
-        } catch (IOException e) {
-            logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
-            return null;
-        }
-        // TODO cache for a day
-        return certificate;
-    }
-
     private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException {
         final var address = resolveSignalServiceAddress(recipientId);
         if (!address.getNumber().isPresent()) {
@@ -850,10 +828,10 @@ public class ManagerImpl implements Manager {
         return registeredUsers;
     }
 
-    private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
+    private void retryFailedReceivedMessages(ReceiveMessageHandler handler) {
         Set<HandleAction> queuedActions = new HashSet<>();
         for (var cachedMessage : account.getMessageCache().getCachedMessages()) {
-            var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage);
+            var actions = retryFailedReceivedMessage(handler, cachedMessage);
             if (actions != null) {
                 queuedActions.addAll(actions);
             }
@@ -862,7 +840,7 @@ public class ManagerImpl implements Manager {
     }
 
     private List<HandleAction> retryFailedReceivedMessage(
-            final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage
+            final ReceiveMessageHandler handler, final CachedMessage cachedMessage
     ) {
         var envelope = cachedMessage.loadEnvelope();
         if (envelope == null) {
@@ -898,14 +876,118 @@ public class ManagerImpl implements Manager {
     }
 
     @Override
-    public void receiveMessages(
-            long timeout,
-            TimeUnit unit,
-            boolean returnOnTimeout,
-            boolean ignoreAttachments,
-            ReceiveMessageHandler handler
+    public void addReceiveHandler(final ReceiveMessageHandler handler) {
+        if (isReceivingSynchronous) {
+            throw new IllegalStateException("Already receiving message synchronously.");
+        }
+        synchronized (messageHandlers) {
+            messageHandlers.add(handler);
+
+            startReceiveThreadIfRequired();
+        }
+    }
+
+    private void startReceiveThreadIfRequired() {
+        if (receiveThread != null) {
+            return;
+        }
+        receiveThread = new Thread(() -> {
+            while (!Thread.interrupted()) {
+                try {
+                    receiveMessagesInternal(1L, TimeUnit.HOURS, false, (envelope, decryptedContent, e) -> {
+                        synchronized (messageHandlers) {
+                            for (ReceiveMessageHandler h : messageHandlers) {
+                                try {
+                                    h.handleMessage(envelope, decryptedContent, e);
+                                } catch (Exception ex) {
+                                    logger.warn("Message handler failed, ignoring", ex);
+                                }
+                            }
+                        }
+                    });
+                    break;
+                } catch (IOException e) {
+                    logger.warn("Receiving messages failed, retrying", e);
+                }
+            }
+            hasCaughtUpWithOldMessages = false;
+            synchronized (messageHandlers) {
+                receiveThread = null;
+
+                // Check if in the meantime another handler has been registered
+                if (!messageHandlers.isEmpty()) {
+                    startReceiveThreadIfRequired();
+                }
+            }
+        });
+
+        receiveThread.start();
+    }
+
+    @Override
+    public void removeReceiveHandler(final ReceiveMessageHandler handler) {
+        final Thread thread;
+        synchronized (messageHandlers) {
+            thread = receiveThread;
+            receiveThread = null;
+            messageHandlers.remove(handler);
+            if (!messageHandlers.isEmpty() || isReceivingSynchronous) {
+                return;
+            }
+        }
+
+        stopReceiveThread(thread);
+    }
+
+    private void stopReceiveThread(final Thread thread) {
+        thread.interrupt();
+        try {
+            thread.join();
+        } catch (InterruptedException ignored) {
+        }
+    }
+
+    @Override
+    public boolean isReceiving() {
+        if (isReceivingSynchronous) {
+            return true;
+        }
+        synchronized (messageHandlers) {
+            return messageHandlers.size() > 0;
+        }
+    }
+
+    @Override
+    public void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException {
+        receiveMessages(timeout, unit, true, handler);
+    }
+
+    @Override
+    public void receiveMessages(ReceiveMessageHandler handler) throws IOException {
+        receiveMessages(1L, TimeUnit.HOURS, false, handler);
+    }
+
+    private void receiveMessages(
+            long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler
+    ) throws IOException {
+        if (isReceiving()) {
+            throw new IllegalStateException("Already receiving message.");
+        }
+        isReceivingSynchronous = true;
+        receiveThread = Thread.currentThread();
+        try {
+            receiveMessagesInternal(timeout, unit, returnOnTimeout, handler);
+        } finally {
+            receiveThread = null;
+            hasCaughtUpWithOldMessages = false;
+            isReceivingSynchronous = false;
+        }
+    }
+
+    private void receiveMessagesInternal(
+            long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler
     ) throws IOException {
-        retryFailedReceivedMessages(handler, ignoreAttachments);
+        retryFailedReceivedMessages(handler);
 
         Set<HandleAction> queuedActions = new HashSet<>();
 
@@ -913,6 +995,8 @@ public class ManagerImpl implements Manager {
         signalWebSocket.connect();
 
         hasCaughtUpWithOldMessages = false;
+        var backOffCounter = 0;
+        final var MAX_BACKOFF_COUNTER = 9;
 
         while (!Thread.interrupted()) {
             SignalServiceEnvelope envelope;
@@ -927,6 +1011,8 @@ public class ManagerImpl implements Manager {
                     // store message on disk, before acknowledging receipt to the server
                     cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
                 });
+                backOffCounter = 0;
+
                 if (result.isPresent()) {
                     envelope = result.get();
                     logger.debug("New message received from server");
@@ -950,11 +1036,24 @@ public class ManagerImpl implements Manager {
                 } else {
                     throw e;
                 }
-            } catch (WebSocketUnavailableException e) {
-                logger.debug("Pipe unexpectedly unavailable, connecting");
-                signalWebSocket.connect();
-                continue;
+            } catch (IOException e) {
+                logger.debug("Pipe unexpectedly unavailable: {}", e.getMessage());
+                if (e instanceof WebSocketUnavailableException || "Connection closed!".equals(e.getMessage())) {
+                    final var sleepMilliseconds = 100 * (long) Math.pow(2, backOffCounter);
+                    backOffCounter = Math.min(backOffCounter + 1, MAX_BACKOFF_COUNTER);
+                    logger.warn("Connection closed unexpectedly, reconnecting in {} ms", sleepMilliseconds);
+                    try {
+                        Thread.sleep(sleepMilliseconds);
+                    } catch (InterruptedException interruptedException) {
+                        return;
+                    }
+                    hasCaughtUpWithOldMessages = false;
+                    signalWebSocket.connect();
+                    continue;
+                }
+                throw e;
             } catch (TimeoutException e) {
+                backOffCounter = 0;
                 if (returnOnTimeout) return;
                 continue;
             }
@@ -965,9 +1064,11 @@ public class ManagerImpl implements Manager {
 
             if (hasCaughtUpWithOldMessages) {
                 handleQueuedActions(queuedActions);
+                queuedActions.clear();
             }
             if (cachedMessage[0] != null) {
                 if (exception instanceof UntrustedIdentityException) {
+                    logger.debug("Keeping message with untrusted identity in message cache");
                     final var address = ((UntrustedIdentityException) exception).getSender();
                     final var recipientId = resolveRecipient(address);
                     if (!envelope.hasSourceUuid()) {
@@ -984,6 +1085,12 @@ public class ManagerImpl implements Manager {
             }
         }
         handleQueuedActions(queuedActions);
+        queuedActions.clear();
+    }
+
+    @Override
+    public void setIgnoreAttachments(final boolean ignoreAttachments) {
+        this.ignoreAttachments = ignoreAttachments;
     }
 
     @Override
@@ -992,6 +1099,7 @@ public class ManagerImpl implements Manager {
     }
 
     private void handleQueuedActions(final Collection<HandleAction> queuedActions) {
+        logger.debug("Handling message actions");
         var interrupted = false;
         for (var action : queuedActions) {
             try {
@@ -1067,7 +1175,7 @@ public class ManagerImpl implements Manager {
         return toGroup(groupHelper.getGroup(groupId));
     }
 
-    public GroupInfo getGroupInfo(GroupId groupId) {
+    private GroupInfo getGroupInfo(GroupId groupId) {
         return groupHelper.getGroup(groupId);
     }
 
@@ -1086,10 +1194,12 @@ public class ManagerImpl implements Manager {
         }
 
         final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId());
+        final var scannableFingerprint = identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(),
+                identityInfo.getIdentityKey());
         return new Identity(address,
                 identityInfo.getIdentityKey(),
-                computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()),
-                computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()),
+                identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()),
+                scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
                 identityInfo.getTrustLevel(),
                 identityInfo.getDateAdded());
     }
@@ -1119,9 +1229,7 @@ public class ManagerImpl implements Manager {
         } catch (UnregisteredUserException e) {
             return false;
         }
-        return trustIdentity(recipientId,
-                identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
-                TrustLevel.TRUSTED_VERIFIED);
+        return identityHelper.trustIdentityVerified(recipientId, fingerprint);
     }
 
     /**
@@ -1138,10 +1246,7 @@ public class ManagerImpl implements Manager {
         } catch (UnregisteredUserException e) {
             return false;
         }
-        var address = resolveSignalServiceAddress(recipientId);
-        return trustIdentity(recipientId,
-                identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
-                TrustLevel.TRUSTED_VERIFIED);
+        return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
     }
 
     /**
@@ -1158,15 +1263,7 @@ public class ManagerImpl implements Manager {
         } catch (UnregisteredUserException e) {
             return false;
         }
-        var address = resolveSignalServiceAddress(recipientId);
-        return trustIdentity(recipientId, identityKey -> {
-            final var fingerprint = computeSafetyNumberFingerprint(address, identityKey);
-            try {
-                return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber);
-            } catch (FingerprintVersionMismatchException | FingerprintParsingException e) {
-                return false;
-            }
-        }, TrustLevel.TRUSTED_VERIFIED);
+        return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
     }
 
     /**
@@ -1182,66 +1279,13 @@ public class ManagerImpl implements Manager {
         } catch (UnregisteredUserException e) {
             return false;
         }
-        return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
-    }
-
-    private boolean trustIdentity(
-            RecipientId recipientId, Function<IdentityKey, Boolean> 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
@@ -1307,6 +1351,15 @@ public class ManagerImpl implements Manager {
     }
 
     private void close(boolean closeAccount) throws IOException {
+        Thread thread;
+        synchronized (messageHandlers) {
+            messageHandlers.clear();
+            thread = receiveThread;
+            receiveThread = null;
+        }
+        if (thread != null) {
+            stopReceiveThread(thread);
+        }
         executor.shutdown();
 
         dependencies.getSignalWebSocket().disconnect();
@@ -1316,5 +1369,4 @@ public class ManagerImpl implements Manager {
         }
         account = null;
     }
-
 }
index 2c85108001e5da33db0c65e610a510ea01a8ad93..d045ac4ac5a4929100e7d2c3984fccd6eb89a8c1 100644 (file)
@@ -2,12 +2,9 @@ package org.asamk.signal.manager;
 
 import java.io.File;
 
-public class PathConfig {
-
-    private final File dataPath;
-    private final File attachmentsPath;
-    private final File avatarsPath;
-    private final File stickerPacksPath;
+public record PathConfig(
+        File dataPath, File attachmentsPath, File avatarsPath, File stickerPacksPath
+) {
 
     public static PathConfig createDefault(final File settingsPath) {
         return new PathConfig(new File(settingsPath, "data"),
@@ -15,29 +12,4 @@ public class PathConfig {
                 new File(settingsPath, "avatars"),
                 new File(settingsPath, "stickers"));
     }
-
-    private PathConfig(
-            final File dataPath, final File attachmentsPath, final File avatarsPath, final File stickerPacksPath
-    ) {
-        this.dataPath = dataPath;
-        this.attachmentsPath = attachmentsPath;
-        this.avatarsPath = avatarsPath;
-        this.stickerPacksPath = stickerPacksPath;
-    }
-
-    public File getDataPath() {
-        return dataPath;
-    }
-
-    public File getAttachmentsPath() {
-        return attachmentsPath;
-    }
-
-    public File getAvatarsPath() {
-        return avatarsPath;
-    }
-
-    public File getStickerPacksPath() {
-        return stickerPacksPath;
-    }
 }
index 226de9be69b71b61cf1bee2157ec93a2d8fcde60..8855e0626ea29b45d04000d2662b1165f307936e 100644 (file)
@@ -95,8 +95,8 @@ public class ProvisioningManager {
 
         logger.info("Received link information from {}, linking in progress ...", number);
 
-        if (SignalAccount.userExists(pathConfig.getDataPath(), number) && !canRelinkExistingAccount(number)) {
-            throw new UserAlreadyExists(number, SignalAccount.getFileName(pathConfig.getDataPath(), number));
+        if (SignalAccount.userExists(pathConfig.dataPath(), number) && !canRelinkExistingAccount(number)) {
+            throw new UserAlreadyExists(number, SignalAccount.getFileName(pathConfig.dataPath(), number));
         }
 
         var encryptedDeviceName = deviceName == null
@@ -115,7 +115,7 @@ public class ProvisioningManager {
 
         SignalAccount account = null;
         try {
-            account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.getDataPath(),
+            account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.dataPath(),
                     number,
                     ret.getUuid(),
                     password,
@@ -134,16 +134,15 @@ public class ProvisioningManager {
                 try {
                     m.refreshPreKeys();
                 } catch (Exception e) {
-                    logger.error("Failed to check new account state.");
-                    throw e;
+                    logger.error("Failed to refresh pre keys.");
                 }
 
                 logger.debug("Requesting sync data");
                 try {
                     m.requestAllSyncData();
                 } catch (Exception e) {
-                    logger.error("Failed to request sync messages from linked device.");
-                    throw e;
+                    logger.error(
+                            "Failed to request sync messages from linked device, data can be requested again with `sendSyncRequest`.");
                 }
 
                 final var result = m;
@@ -166,7 +165,7 @@ public class ProvisioningManager {
     private boolean canRelinkExistingAccount(final String number) throws IOException {
         final SignalAccount signalAccount;
         try {
-            signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false, TrustNewIdentity.ON_FIRST_USE);
+            signalAccount = SignalAccount.load(pathConfig.dataPath(), number, false, TrustNewIdentity.ON_FIRST_USE);
         } catch (IOException e) {
             logger.debug("Account in use or failed to load.", e);
             return false;
index c42782f718d4b4a8f5d8cc4b825f25acde19f079..8a96dad0c6f28d160dae265e59c8382e1ca9c94a 100644 (file)
@@ -96,12 +96,12 @@ public class RegistrationManager implements Closeable {
         var pathConfig = PathConfig.createDefault(settingsPath);
 
         final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
-        if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) {
+        if (!SignalAccount.userExists(pathConfig.dataPath(), number)) {
             var identityKey = KeyUtils.generateIdentityKeyPair();
             var registrationId = KeyHelper.generateRegistrationId(false);
 
             var profileKey = KeyUtils.createProfileKey();
-            var account = SignalAccount.create(pathConfig.getDataPath(),
+            var account = SignalAccount.create(pathConfig.dataPath(),
                     number,
                     identityKey,
                     registrationId,
@@ -111,7 +111,7 @@ public class RegistrationManager implements Closeable {
             return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
         }
 
-        var account = SignalAccount.load(pathConfig.getDataPath(), number, true, TrustNewIdentity.ON_FIRST_USE);
+        var account = SignalAccount.load(pathConfig.dataPath(), number, true, TrustNewIdentity.ON_FIRST_USE);
 
         return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
     }
index 24a673ff867508e68046ab386bd9495fa85abd23..b905f9b70d378bfb9a8f561e6ec5d6cee7673803 100644 (file)
@@ -63,15 +63,9 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor {
 
     private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) {
         switch (connectionState) {
-            case CONNECTED:
-                logger.debug("WebSocket is now connected");
-                break;
-            case AUTHENTICATION_FAILED:
-                logger.debug("WebSocket authentication failed");
-                break;
-            case FAILED:
-                logger.debug("WebSocket connection failed");
-                break;
+            case CONNECTED -> logger.debug("WebSocket is now connected");
+            case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed");
+            case FAILED -> logger.debug("WebSocket connection failed");
         }
 
         healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
@@ -101,6 +95,7 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor {
             if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
                 logger.warn("Received too many mismatch device errors, forcing new websockets.");
                 signalWebSocket.forceNewWebSockets();
+                signalWebSocket.connect();
             }
         }
     }
@@ -146,6 +141,7 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor {
                                     + " needed by: "
                                     + keepAliveRequiredSinceTime);
                             signalWebSocket.forceNewWebSockets();
+                            signalWebSocket.connect();
                         } else {
                             signalWebSocket.sendKeepAlive();
                         }
index 5c712866228db229dc0503e6f0d2881775e10e4d..fead442cd2f537f1cda9cd244120301d5f41045a 100644 (file)
@@ -18,40 +18,27 @@ public enum TrustLevel {
     }
 
     public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) {
-        switch (identityState) {
-            case DEFAULT:
-                return TRUSTED_UNVERIFIED;
-            case UNVERIFIED:
-                return UNTRUSTED;
-            case VERIFIED:
-                return TRUSTED_VERIFIED;
-            case UNRECOGNIZED:
-                return null;
-        }
-        throw new RuntimeException("Unknown identity state: " + identityState);
+        return switch (identityState) {
+            case DEFAULT -> TRUSTED_UNVERIFIED;
+            case UNVERIFIED -> UNTRUSTED;
+            case VERIFIED -> TRUSTED_VERIFIED;
+            case UNRECOGNIZED -> null;
+        };
     }
 
     public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) {
-        switch (verifiedState) {
-            case DEFAULT:
-                return TRUSTED_UNVERIFIED;
-            case UNVERIFIED:
-                return UNTRUSTED;
-            case VERIFIED:
-                return TRUSTED_VERIFIED;
-        }
-        throw new RuntimeException("Unknown verified state: " + verifiedState);
+        return switch (verifiedState) {
+            case DEFAULT -> TRUSTED_UNVERIFIED;
+            case UNVERIFIED -> UNTRUSTED;
+            case VERIFIED -> TRUSTED_VERIFIED;
+        };
     }
 
     public VerifiedMessage.VerifiedState toVerifiedState() {
-        switch (this) {
-            case TRUSTED_UNVERIFIED:
-                return VerifiedMessage.VerifiedState.DEFAULT;
-            case UNTRUSTED:
-                return VerifiedMessage.VerifiedState.UNVERIFIED;
-            case TRUSTED_VERIFIED:
-                return VerifiedMessage.VerifiedState.VERIFIED;
-        }
-        throw new RuntimeException("Unknown verified state: " + this);
+        return switch (this) {
+            case TRUSTED_UNVERIFIED -> VerifiedMessage.VerifiedState.DEFAULT;
+            case UNTRUSTED -> VerifiedMessage.VerifiedState.UNVERIFIED;
+            case TRUSTED_VERIFIED -> VerifiedMessage.VerifiedState.VERIFIED;
+        };
     }
 }
index ecd5597d57519d9427f501f93331ad237a1bbabb..3ecd9a1e1d04d5ead4c5c3697d49a07e3f540dce 100644 (file)
@@ -54,17 +54,12 @@ public class SendRetryMessageRequestAction implements HandleAction {
     }
 
     private static int envelopeTypeToCiphertextMessageType(int envelopeType) {
-        switch (envelopeType) {
-            case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE:
-                return CiphertextMessage.PREKEY_TYPE;
-            case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE:
-                return CiphertextMessage.SENDERKEY_TYPE;
-            case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE:
-                return CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
-            case SignalServiceProtos.Envelope.Type.CIPHERTEXT_VALUE:
-            default:
-                return CiphertextMessage.WHISPER_TYPE;
-        }
+        return switch (envelopeType) {
+            case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE -> CiphertextMessage.PREKEY_TYPE;
+            case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE -> CiphertextMessage.SENDERKEY_TYPE;
+            case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
+            default -> CiphertextMessage.WHISPER_TYPE;
+        };
     }
 
     @Override
index 9ee0d36a7b1641aa7fcc54e1b4b0932d392c57e3..bd67052d681f80d04c21c0c07d92f5800b58b3d5 100644 (file)
@@ -1,38 +1,3 @@
 package org.asamk.signal.manager.api;
 
-public class Device {
-
-    private final long id;
-    private final String name;
-    private final long created;
-    private final long lastSeen;
-    private final boolean thisDevice;
-
-    public Device(long id, String name, long created, long lastSeen, final boolean thisDevice) {
-        this.id = id;
-        this.name = name;
-        this.created = created;
-        this.lastSeen = lastSeen;
-        this.thisDevice = thisDevice;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public long getCreated() {
-        return created;
-    }
-
-    public long getLastSeen() {
-        return lastSeen;
-    }
-
-    public boolean isThisDevice() {
-        return thisDevice;
-    }
-}
+public record Device(long id, String name, long created, long lastSeen, boolean isThisDevice) {}
index 650e10b6eeffcde5eee2fe45001fe4eba66743e7..88be3539b8971a3458f4289500c106dd56b4404c 100644 (file)
@@ -2,98 +2,25 @@ package org.asamk.signal.manager.api;
 
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
 
 import java.util.Set;
 
-public class Group {
-
-    private final GroupId groupId;
-    private final String title;
-    private final String description;
-    private final GroupInviteLinkUrl groupInviteLinkUrl;
-    private final Set<RecipientAddress> members;
-    private final Set<RecipientAddress> pendingMembers;
-    private final Set<RecipientAddress> requestingMembers;
-    private final Set<RecipientAddress> adminMembers;
-    private final boolean isBlocked;
-    private final int messageExpirationTime;
-    private final boolean isAnnouncementGroup;
-    private final boolean isMember;
-
-    public Group(
-            final GroupId groupId,
-            final String title,
-            final String description,
-            final GroupInviteLinkUrl groupInviteLinkUrl,
-            final Set<RecipientAddress> members,
-            final Set<RecipientAddress> pendingMembers,
-            final Set<RecipientAddress> requestingMembers,
-            final Set<RecipientAddress> adminMembers,
-            final boolean isBlocked,
-            final int messageExpirationTime,
-            final boolean isAnnouncementGroup,
-            final boolean isMember
-    ) {
-        this.groupId = groupId;
-        this.title = title;
-        this.description = description;
-        this.groupInviteLinkUrl = groupInviteLinkUrl;
-        this.members = members;
-        this.pendingMembers = pendingMembers;
-        this.requestingMembers = requestingMembers;
-        this.adminMembers = adminMembers;
-        this.isBlocked = isBlocked;
-        this.messageExpirationTime = messageExpirationTime;
-        this.isAnnouncementGroup = isAnnouncementGroup;
-        this.isMember = isMember;
-    }
-
-    public GroupId getGroupId() {
-        return groupId;
-    }
-
-    public String getTitle() {
-        return title;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public GroupInviteLinkUrl getGroupInviteLinkUrl() {
-        return groupInviteLinkUrl;
-    }
-
-    public Set<RecipientAddress> getMembers() {
-        return members;
-    }
-
-    public Set<RecipientAddress> getPendingMembers() {
-        return pendingMembers;
-    }
-
-    public Set<RecipientAddress> getRequestingMembers() {
-        return requestingMembers;
-    }
-
-    public Set<RecipientAddress> getAdminMembers() {
-        return adminMembers;
-    }
-
-    public boolean isBlocked() {
-        return isBlocked;
-    }
-
-    public int getMessageExpirationTime() {
-        return messageExpirationTime;
-    }
-
-    public boolean isAnnouncementGroup() {
-        return isAnnouncementGroup;
-    }
-
-    public boolean isMember() {
-        return isMember;
-    }
-}
+public record Group(
+        GroupId groupId,
+        String title,
+        String description,
+        GroupInviteLinkUrl groupInviteLinkUrl,
+        Set<RecipientAddress> members,
+        Set<RecipientAddress> pendingMembers,
+        Set<RecipientAddress> requestingMembers,
+        Set<RecipientAddress> adminMembers,
+        boolean isBlocked,
+        int messageExpirationTimer,
+        GroupPermission permissionAddMember,
+        GroupPermission permissionEditDetails,
+        GroupPermission permissionSendMessage,
+        boolean isMember,
+        boolean isAdmin
+) {}
index 4f6f21f6c8b55501635b7b859060eb558488035c..37d298f9b2e1ce606cbb90254f22f8e029cbcce8 100644 (file)
@@ -6,60 +6,16 @@ import org.whispersystems.libsignal.IdentityKey;
 
 import java.util.Date;
 
-public class Identity {
-
-    private final RecipientAddress recipient;
-    private final IdentityKey identityKey;
-    private final String safetyNumber;
-    private final byte[] scannableSafetyNumber;
-    private final TrustLevel trustLevel;
-    private final Date dateAdded;
-
-    public Identity(
-            final RecipientAddress recipient,
-            final IdentityKey identityKey,
-            final String safetyNumber,
-            final byte[] scannableSafetyNumber,
-            final TrustLevel trustLevel,
-            final Date dateAdded
-    ) {
-        this.recipient = recipient;
-        this.identityKey = identityKey;
-        this.safetyNumber = safetyNumber;
-        this.scannableSafetyNumber = scannableSafetyNumber;
-        this.trustLevel = trustLevel;
-        this.dateAdded = dateAdded;
-    }
-
-    public RecipientAddress getRecipient() {
-        return recipient;
-    }
-
-    public IdentityKey getIdentityKey() {
-        return this.identityKey;
-    }
-
-    public TrustLevel getTrustLevel() {
-        return this.trustLevel;
-    }
-
-    boolean isTrusted() {
-        return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED;
-    }
-
-    public Date getDateAdded() {
-        return this.dateAdded;
-    }
+public record Identity(
+        RecipientAddress recipient,
+        IdentityKey identityKey,
+        String safetyNumber,
+        byte[] scannableSafetyNumber,
+        TrustLevel trustLevel,
+        Date dateAdded
+) {
 
     public byte[] getFingerprint() {
         return identityKey.getPublicKey().serialize();
     }
-
-    public String getSafetyNumber() {
-        return safetyNumber;
-    }
-
-    public byte[] getScannableSafetyNumber() {
-        return scannableSafetyNumber;
-    }
 }
index dee18524ebb14ede930e1e0378c31db807d4621a..8366600352ebaa60004bd138e9bb57c57403fb1f 100644 (file)
@@ -2,21 +2,4 @@ package org.asamk.signal.manager.api;
 
 import java.util.List;
 
-public class Message {
-
-    private final String messageText;
-    private final List<String> attachments;
-
-    public Message(final String messageText, final List<String> attachments) {
-        this.messageText = messageText;
-        this.attachments = attachments;
-    }
-
-    public String getMessageText() {
-        return messageText;
-    }
-
-    public List<String> getAttachments() {
-        return attachments;
-    }
-}
+public record Message(String messageText, List<String> attachments) {}
index be1029e69be4baedc4048103dc82a2f3150e40e9..ec2d00f555e11a8aee146e7d9041728fee750113 100644 (file)
@@ -9,9 +9,9 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
 
 import java.util.UUID;
 
-public abstract class RecipientIdentifier {
+public sealed abstract class RecipientIdentifier {
 
-    public static class NoteToSelf extends RecipientIdentifier {
+    public static final class NoteToSelf extends RecipientIdentifier {
 
         public static NoteToSelf INSTANCE = new NoteToSelf();
 
@@ -19,7 +19,7 @@ public abstract class RecipientIdentifier {
         }
     }
 
-    public abstract static class Single extends RecipientIdentifier {
+    public sealed static abstract class Single extends RecipientIdentifier {
 
         public static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
             return UuidUtil.isUuid(identifier)
@@ -43,7 +43,7 @@ public abstract class RecipientIdentifier {
         public abstract String getIdentifier();
     }
 
-    public static class Uuid extends Single {
+    public static final class Uuid extends Single {
 
         public final UUID uuid;
 
@@ -72,7 +72,7 @@ public abstract class RecipientIdentifier {
         }
     }
 
-    public static class Number extends Single {
+    public static final class Number extends Single {
 
         public final String number;
 
@@ -101,7 +101,7 @@ public abstract class RecipientIdentifier {
         }
     }
 
-    public static class Group extends RecipientIdentifier {
+    public static final class Group extends RecipientIdentifier {
 
         public final GroupId groupId;
 
index d5c9ef910eeb5a72937411b197d699c6e53babbe..54f55b920810e5c7211521f7efdc69a873c8f281 100644 (file)
@@ -4,23 +4,4 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
 
 import java.util.List;
 
-public class SendGroupMessageResults {
-
-    private final long timestamp;
-    private final List<SendMessageResult> results;
-
-    public SendGroupMessageResults(
-            final long timestamp, final List<SendMessageResult> results
-    ) {
-        this.timestamp = timestamp;
-        this.results = results;
-    }
-
-    public long getTimestamp() {
-        return timestamp;
-    }
-
-    public List<SendMessageResult> getResults() {
-        return results;
-    }
-}
+public record SendGroupMessageResults(long timestamp, List<SendMessageResult> results) {}
index ff3239194106934481c8b2e70145d0eb6606117b..064532c04fabb5bbea375303486a6663207fd109 100644 (file)
@@ -5,23 +5,4 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
 import java.util.List;
 import java.util.Map;
 
-public class SendMessageResults {
-
-    private final long timestamp;
-    private final Map<RecipientIdentifier, List<SendMessageResult>> results;
-
-    public SendMessageResults(
-            final long timestamp, final Map<RecipientIdentifier, List<SendMessageResult>> results
-    ) {
-        this.timestamp = timestamp;
-        this.results = results;
-    }
-
-    public long getTimestamp() {
-        return timestamp;
-    }
-
-    public Map<RecipientIdentifier, List<SendMessageResult>> getResults() {
-        return results;
-    }
-}
+public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {}
index 228990c19cd3466daa6855ffe5322a7436284fcd..86a858f01cd8d96252a13aae6f4ecf7f81a8274e 100644 (file)
@@ -7,13 +7,9 @@ public enum TypingAction {
     STOP;
 
     public SignalServiceTypingMessage.Action toSignalService() {
-        switch (this) {
-            case START:
-                return SignalServiceTypingMessage.Action.STARTED;
-            case STOP:
-                return SignalServiceTypingMessage.Action.STOPPED;
-            default:
-                throw new IllegalStateException("Invalid typing action " + this);
-        }
+        return switch (this) {
+            case START -> SignalServiceTypingMessage.Action.STARTED;
+            case STOP -> SignalServiceTypingMessage.Action.STOPPED;
+        };
     }
 }
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java b/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java
new file mode 100644 (file)
index 0000000..b5877ae
--- /dev/null
@@ -0,0 +1,203 @@
+package org.asamk.signal.manager.api;
+
+import org.asamk.signal.manager.groups.GroupLinkState;
+import org.asamk.signal.manager.groups.GroupPermission;
+
+import java.io.File;
+import java.util.Set;
+
+public class UpdateGroup {
+
+    private final String name;
+    private final String description;
+    private final Set<RecipientIdentifier.Single> members;
+    private final Set<RecipientIdentifier.Single> removeMembers;
+    private final Set<RecipientIdentifier.Single> admins;
+    private final Set<RecipientIdentifier.Single> removeAdmins;
+    private final boolean resetGroupLink;
+    private final GroupLinkState groupLinkState;
+    private final GroupPermission addMemberPermission;
+    private final GroupPermission editDetailsPermission;
+    private final File avatarFile;
+    private final Integer expirationTimer;
+    private final Boolean isAnnouncementGroup;
+
+    private UpdateGroup(final Builder builder) {
+        name = builder.name;
+        description = builder.description;
+        members = builder.members;
+        removeMembers = builder.removeMembers;
+        admins = builder.admins;
+        removeAdmins = builder.removeAdmins;
+        resetGroupLink = builder.resetGroupLink;
+        groupLinkState = builder.groupLinkState;
+        addMemberPermission = builder.addMemberPermission;
+        editDetailsPermission = builder.editDetailsPermission;
+        avatarFile = builder.avatarFile;
+        expirationTimer = builder.expirationTimer;
+        isAnnouncementGroup = builder.isAnnouncementGroup;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static Builder newBuilder(final UpdateGroup copy) {
+        Builder builder = new Builder();
+        builder.name = copy.getName();
+        builder.description = copy.getDescription();
+        builder.members = copy.getMembers();
+        builder.removeMembers = copy.getRemoveMembers();
+        builder.admins = copy.getAdmins();
+        builder.removeAdmins = copy.getRemoveAdmins();
+        builder.resetGroupLink = copy.isResetGroupLink();
+        builder.groupLinkState = copy.getGroupLinkState();
+        builder.addMemberPermission = copy.getAddMemberPermission();
+        builder.editDetailsPermission = copy.getEditDetailsPermission();
+        builder.avatarFile = copy.getAvatarFile();
+        builder.expirationTimer = copy.getExpirationTimer();
+        builder.isAnnouncementGroup = copy.getIsAnnouncementGroup();
+        return builder;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public Set<RecipientIdentifier.Single> getMembers() {
+        return members;
+    }
+
+    public Set<RecipientIdentifier.Single> getRemoveMembers() {
+        return removeMembers;
+    }
+
+    public Set<RecipientIdentifier.Single> getAdmins() {
+        return admins;
+    }
+
+    public Set<RecipientIdentifier.Single> getRemoveAdmins() {
+        return removeAdmins;
+    }
+
+    public boolean isResetGroupLink() {
+        return resetGroupLink;
+    }
+
+    public GroupLinkState getGroupLinkState() {
+        return groupLinkState;
+    }
+
+    public GroupPermission getAddMemberPermission() {
+        return addMemberPermission;
+    }
+
+    public GroupPermission getEditDetailsPermission() {
+        return editDetailsPermission;
+    }
+
+    public File getAvatarFile() {
+        return avatarFile;
+    }
+
+    public Integer getExpirationTimer() {
+        return expirationTimer;
+    }
+
+    public Boolean getIsAnnouncementGroup() {
+        return isAnnouncementGroup;
+    }
+
+    public static final class Builder {
+
+        private String name;
+        private String description;
+        private Set<RecipientIdentifier.Single> members;
+        private Set<RecipientIdentifier.Single> removeMembers;
+        private Set<RecipientIdentifier.Single> admins;
+        private Set<RecipientIdentifier.Single> removeAdmins;
+        private boolean resetGroupLink;
+        private GroupLinkState groupLinkState;
+        private GroupPermission addMemberPermission;
+        private GroupPermission editDetailsPermission;
+        private File avatarFile;
+        private Integer expirationTimer;
+        private Boolean isAnnouncementGroup;
+
+        private Builder() {
+        }
+
+        public Builder withName(final String val) {
+            name = val;
+            return this;
+        }
+
+        public Builder withDescription(final String val) {
+            description = val;
+            return this;
+        }
+
+        public Builder withMembers(final Set<RecipientIdentifier.Single> val) {
+            members = val;
+            return this;
+        }
+
+        public Builder withRemoveMembers(final Set<RecipientIdentifier.Single> val) {
+            removeMembers = val;
+            return this;
+        }
+
+        public Builder withAdmins(final Set<RecipientIdentifier.Single> val) {
+            admins = val;
+            return this;
+        }
+
+        public Builder withRemoveAdmins(final Set<RecipientIdentifier.Single> val) {
+            removeAdmins = val;
+            return this;
+        }
+
+        public Builder withResetGroupLink(final boolean val) {
+            resetGroupLink = val;
+            return this;
+        }
+
+        public Builder withGroupLinkState(final GroupLinkState val) {
+            groupLinkState = val;
+            return this;
+        }
+
+        public Builder withAddMemberPermission(final GroupPermission val) {
+            addMemberPermission = val;
+            return this;
+        }
+
+        public Builder withEditDetailsPermission(final GroupPermission val) {
+            editDetailsPermission = val;
+            return this;
+        }
+
+        public Builder withAvatarFile(final File val) {
+            avatarFile = val;
+            return this;
+        }
+
+        public Builder withExpirationTimer(final Integer val) {
+            expirationTimer = val;
+            return this;
+        }
+
+        public Builder withIsAnnouncementGroup(final Boolean val) {
+            isAnnouncementGroup = val;
+            return this;
+        }
+
+        public UpdateGroup build() {
+            return new UpdateGroup(this);
+        }
+    }
+}
index 177f6697cf5a6485fa63913e81e6cdb9742fe8a0..0da1f2f9e3901b01e5b63dc293f4051c6d525e78 100644 (file)
@@ -46,7 +46,7 @@ class LiveConfig {
     private final static Optional<SignalProxy> proxy = Optional.absent();
 
     private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
-            .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=");
+            .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY");
 
     static SignalServiceConfiguration createDefaultServiceConfiguration(
             final List<Interceptor> interceptors
index d643f10a1f087252e6f5dc639b52bdca42226477..975c95d3e9fcc8007bebd633c62d98caab59ab89 100644 (file)
@@ -46,7 +46,7 @@ class SandboxConfig {
     private final static Optional<SignalProxy> proxy = Optional.absent();
 
     private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
-            .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=");
+            .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARB");
 
     static SignalServiceConfiguration createDefaultServiceConfiguration(
             final List<Interceptor> interceptors
index a9a08d93e8d91d353c89844a9f863d441d29866e..2634a59342e9976d46d2e78d1e5c25339dafaf81 100644 (file)
@@ -88,19 +88,15 @@ public class ServiceConfig {
 
         final var interceptors = List.of(userAgentInterceptor);
 
-        switch (serviceEnvironment) {
-            case LIVE:
-                return new ServiceEnvironmentConfig(LiveConfig.createDefaultServiceConfiguration(interceptors),
-                        LiveConfig.getUnidentifiedSenderTrustRoot(),
-                        LiveConfig.createKeyBackupConfig(),
-                        LiveConfig.getCdsMrenclave());
-            case SANDBOX:
-                return new ServiceEnvironmentConfig(SandboxConfig.createDefaultServiceConfiguration(interceptors),
-                        SandboxConfig.getUnidentifiedSenderTrustRoot(),
-                        SandboxConfig.createKeyBackupConfig(),
-                        SandboxConfig.getCdsMrenclave());
-            default:
-                throw new IllegalArgumentException("Unsupported environment");
-        }
+        return switch (serviceEnvironment) {
+            case LIVE -> new ServiceEnvironmentConfig(LiveConfig.createDefaultServiceConfiguration(interceptors),
+                    LiveConfig.getUnidentifiedSenderTrustRoot(),
+                    LiveConfig.createKeyBackupConfig(),
+                    LiveConfig.getCdsMrenclave());
+            case SANDBOX -> new ServiceEnvironmentConfig(SandboxConfig.createDefaultServiceConfiguration(interceptors),
+                    SandboxConfig.getUnidentifiedSenderTrustRoot(),
+                    SandboxConfig.createKeyBackupConfig(),
+                    SandboxConfig.getCdsMrenclave());
+        };
     }
 }
index e7e1b5f58d8d5a768118bec8ea432c9fd6abdd73..09d5162e5546941a8c015c778f1a9ca5fb8f8792 100644 (file)
@@ -62,29 +62,9 @@ public class ConfigurationStore {
         return new Storage(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews);
     }
 
-    public static final class Storage {
-
-        public Boolean readReceipts;
-        public Boolean unidentifiedDeliveryIndicators;
-        public Boolean typingIndicators;
-        public Boolean linkPreviews;
-
-        // For deserialization
-        private Storage() {
-        }
-
-        public Storage(
-                final Boolean readReceipts,
-                final Boolean unidentifiedDeliveryIndicators,
-                final Boolean typingIndicators,
-                final Boolean linkPreviews
-        ) {
-            this.readReceipts = readReceipts;
-            this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
-            this.typingIndicators = typingIndicators;
-            this.linkPreviews = linkPreviews;
-        }
-    }
+    public record Storage(
+            Boolean readReceipts, Boolean unidentifiedDeliveryIndicators, Boolean typingIndicators, Boolean linkPreviews
+    ) {}
 
     public interface Saver {
 
index 9ecb963092dba37fc4c05aab9ec892ab2f05bdb5..38ddd4b62cca85766dd96aecff679efd0b198443 100644 (file)
@@ -3,7 +3,7 @@ package org.asamk.signal.manager.groups;
 import java.util.Arrays;
 import java.util.Base64;
 
-public abstract class GroupId {
+public abstract sealed class GroupId permits GroupIdV1, GroupIdV2 {
 
     private final byte[] id;
 
index d2012fa0fec525b4e58454a6838c0d9908edb21c..acec587b99c1e42d7208e08e73c20e28cf718a68 100644 (file)
@@ -4,7 +4,7 @@ import java.util.Base64;
 
 import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
 
-public class GroupIdV1 extends GroupId {
+public final class GroupIdV1 extends GroupId {
 
     public static GroupIdV1 createRandom() {
         return new GroupIdV1(getSecretBytes(16));
index 913a9e936a69973a6882405e83feca979cbd3a48..35aac3304545e9569f1ba3b3a5b3fdf01d0deffa 100644 (file)
@@ -2,7 +2,7 @@ package org.asamk.signal.manager.groups;
 
 import java.util.Base64;
 
-public class GroupIdV2 extends GroupId {
+public final class GroupIdV2 extends GroupId {
 
     public static GroupIdV2 fromBase64(String groupId) {
         return new GroupIdV2(Base64.getDecoder().decode(groupId));
index dd9dd2d265434ff639ed53b55196f1ee0c2de2ae..0498fba18454b7da18d1e892b8d80d0569df1bbf 100644 (file)
@@ -56,7 +56,7 @@ public final class GroupInviteLinkUrl {
             var groupInviteLink = GroupInviteLink.parseFrom(bytes);
 
             switch (groupInviteLink.getContentsCase()) {
-                case V1CONTENTS: {
+                case V1CONTENTS -> {
                     var groupInviteLinkContentsV1 = groupInviteLink.getV1Contents();
                     var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey()
                             .toByteArray());
@@ -65,8 +65,7 @@ public final class GroupInviteLinkUrl {
 
                     return new GroupInviteLinkUrl(groupMasterKey, password);
                 }
-                default:
-                    throw new UnknownGroupLinkVersionException("Url contains no known group link content");
+                default -> throw new UnknownGroupLinkVersionException("Url contains no known group link content");
             }
         } catch (InvalidInputException | IOException e) {
             throw new InvalidGroupLinkException(e);
index 62f4f11134b7169f747c24a522129c08e708c570..dbee3e842e09fc32018e39795b418a6fa19fa8d8 100644 (file)
@@ -351,8 +351,7 @@ public class GroupHelper {
 
     private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
         final var group = account.getGroupStore().getGroup(groupId);
-        if (group instanceof GroupInfoV2) {
-            final var groupInfoV2 = (GroupInfoV2) group;
+        if (group instanceof GroupInfoV2 groupInfoV2) {
             if (forceUpdate || (!groupInfoV2.isPermissionDenied() && groupInfoV2.getGroup() == null)) {
                 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
                 DecryptedGroup decryptedGroup;
@@ -639,7 +638,7 @@ public class GroupHelper {
 
         return SignalServiceDataMessage.newBuilder()
                 .asGroupMessage(group.build())
-                .withExpiration(g.getMessageExpirationTime());
+                .withExpiration(g.getMessageExpirationTimer());
     }
 
     private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
@@ -648,7 +647,7 @@ public class GroupHelper {
                 .withSignedGroupChange(signedGroupChange);
         return SignalServiceDataMessage.newBuilder()
                 .asGroupMessage(group.build())
-                .withExpiration(g.getMessageExpirationTime());
+                .withExpiration(g.getMessageExpirationTimer());
     }
 
     private SendGroupMessageResults sendUpdateGroupV2Message(
index 746af2f91ecc9f5bd3d93b9aa77e2dd09261e779..f526a596f8d9bc44ba64ef0be9e3c30bfb3ad66a 100644 (file)
@@ -391,27 +391,18 @@ public class GroupV2Helper {
     }
 
     private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
-        switch (state) {
-            case DISABLED:
-                return AccessControl.AccessRequired.UNSATISFIABLE;
-            case ENABLED:
-                return AccessControl.AccessRequired.ANY;
-            case ENABLED_WITH_APPROVAL:
-                return AccessControl.AccessRequired.ADMINISTRATOR;
-            default:
-                throw new AssertionError();
-        }
+        return switch (state) {
+            case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE;
+            case ENABLED -> AccessControl.AccessRequired.ANY;
+            case ENABLED_WITH_APPROVAL -> AccessControl.AccessRequired.ADMINISTRATOR;
+        };
     }
 
     private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) {
-        switch (permission) {
-            case EVERY_MEMBER:
-                return AccessControl.AccessRequired.MEMBER;
-            case ONLY_ADMINS:
-                return AccessControl.AccessRequired.ADMINISTRATOR;
-            default:
-                throw new AssertionError();
-        }
+        return switch (permission) {
+            case EVERY_MEMBER -> AccessControl.AccessRequired.MEMBER;
+            case ONLY_ADMINS -> AccessControl.AccessRequired.ADMINISTRATOR;
+        };
     }
 
     private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) {
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java
new file mode 100644 (file)
index 0000000..531870d
--- /dev/null
@@ -0,0 +1,135 @@
+package org.asamk.signal.manager.helper;
+
+import org.asamk.signal.manager.SignalDependencies;
+import org.asamk.signal.manager.TrustLevel;
+import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.asamk.signal.manager.util.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.fingerprint.Fingerprint;
+import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
+import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
+import org.whispersystems.libsignal.fingerprint.ScannableFingerprint;
+import org.whispersystems.signalservice.api.messages.SendMessageResult;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.function.Function;
+
+import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
+
+public class IdentityHelper {
+
+    private final static Logger logger = LoggerFactory.getLogger(IdentityHelper.class);
+
+    private final SignalAccount account;
+    private final SignalDependencies dependencies;
+    private final SignalServiceAddressResolver addressResolver;
+    private final SyncHelper syncHelper;
+    private final ProfileHelper profileHelper;
+
+    public IdentityHelper(
+            final SignalAccount account,
+            final SignalDependencies dependencies,
+            final SignalServiceAddressResolver addressResolver,
+            final SyncHelper syncHelper,
+            final ProfileHelper profileHelper
+    ) {
+        this.account = account;
+        this.dependencies = dependencies;
+        this.addressResolver = addressResolver;
+        this.syncHelper = syncHelper;
+        this.profileHelper = profileHelper;
+    }
+
+    public boolean trustIdentityVerified(RecipientId recipientId, byte[] fingerprint) {
+        return trustIdentity(recipientId,
+                identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
+                TrustLevel.TRUSTED_VERIFIED);
+    }
+
+    public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, String safetyNumber) {
+        return trustIdentity(recipientId,
+                identityKey -> safetyNumber.equals(computeSafetyNumber(recipientId, identityKey)),
+                TrustLevel.TRUSTED_VERIFIED);
+    }
+
+    public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, byte[] safetyNumber) {
+        return trustIdentity(recipientId, identityKey -> {
+            final var fingerprint = computeSafetyNumberForScanning(recipientId, identityKey);
+            try {
+                return fingerprint != null && fingerprint.compareTo(safetyNumber);
+            } catch (FingerprintVersionMismatchException | FingerprintParsingException e) {
+                return false;
+            }
+        }, TrustLevel.TRUSTED_VERIFIED);
+    }
+
+    public boolean trustIdentityAllKeys(RecipientId recipientId) {
+        return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
+    }
+
+    public String computeSafetyNumber(RecipientId recipientId, IdentityKey theirIdentityKey) {
+        var address = addressResolver.resolveSignalServiceAddress(recipientId);
+        final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey);
+        return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText();
+    }
+
+    public ScannableFingerprint computeSafetyNumberForScanning(RecipientId recipientId, IdentityKey theirIdentityKey) {
+        var address = addressResolver.resolveSignalServiceAddress(recipientId);
+        final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey);
+        return fingerprint == null ? null : fingerprint.getScannableFingerprint();
+    }
+
+    private Fingerprint computeSafetyNumberFingerprint(
+            final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
+    ) {
+        return Utils.computeSafetyNumber(capabilities.isUuid(),
+                account.getSelfAddress(),
+                account.getIdentityKeyPair().getPublicKey(),
+                theirAddress,
+                theirIdentityKey);
+    }
+
+    private boolean trustIdentity(
+            RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
+    ) {
+        var identity = account.getIdentityKeyStore().getIdentity(recipientId);
+        if (identity == null) {
+            return false;
+        }
+
+        if (!verifier.apply(identity.getIdentityKey())) {
+            return false;
+        }
+
+        account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
+        try {
+            var address = addressResolver.resolveSignalServiceAddress(recipientId);
+            syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
+        } catch (IOException e) {
+            logger.warn("Failed to send verification sync message: {}", e.getMessage());
+        }
+
+        return true;
+    }
+
+    public void handleIdentityFailure(
+            final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure
+    ) {
+        final var identityKey = identityFailure.getIdentityKey();
+        if (identityKey != null) {
+            final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
+            if (newIdentity) {
+                account.getSessionStore().archiveSessions(recipientId);
+            }
+        } else {
+            // Retrieve profile to get the current identity key from the server
+            profileHelper.refreshRecipientProfile(recipientId);
+        }
+    }
+}
index 16f47d3c8458e993e21025e915ae006e8394543e..47aa6156bb40f9e277143f0ca9c7ff5d0fd05a4f 100644 (file)
@@ -358,11 +358,19 @@ public final class IncomingMessageHandler {
         if (syncMessage.getConfiguration().isPresent()) {
             final var configurationMessage = syncMessage.getConfiguration().get();
             final var configurationStore = account.getConfigurationStore();
-            configurationStore.setReadReceipts(configurationMessage.getReadReceipts().orNull());
-            configurationStore.setLinkPreviews(configurationMessage.getLinkPreviews().orNull());
-            configurationStore.setTypingIndicators(configurationMessage.getTypingIndicators().orNull());
-            configurationStore.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators()
-                    .orNull());
+            if (configurationMessage.getReadReceipts().isPresent()) {
+                configurationStore.setReadReceipts(configurationMessage.getReadReceipts().get());
+            }
+            if (configurationMessage.getLinkPreviews().isPresent()) {
+                configurationStore.setLinkPreviews(configurationMessage.getLinkPreviews().get());
+            }
+            if (configurationMessage.getTypingIndicators().isPresent()) {
+                configurationStore.setTypingIndicators(configurationMessage.getTypingIndicators().get());
+            }
+            if (configurationMessage.getUnidentifiedDeliveryIndicators().isPresent()) {
+                configurationStore.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators()
+                        .get());
+            }
         }
         return actions;
     }
index e24d41fad91464144f11f8e5ed67ad6e565c4ecc..96188c4e351b3330f7faa992daf5ca7114edcf4d 100644 (file)
@@ -33,6 +33,7 @@ import java.util.Base64;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 
@@ -45,7 +46,6 @@ public final class ProfileHelper {
     private final SignalAccount account;
     private final SignalDependencies dependencies;
     private final AvatarStore avatarStore;
-    private final ProfileKeyProvider profileKeyProvider;
     private final UnidentifiedAccessProvider unidentifiedAccessProvider;
     private final SignalServiceAddressResolver addressResolver;
 
@@ -53,14 +53,12 @@ public final class ProfileHelper {
             final SignalAccount account,
             final SignalDependencies dependencies,
             final AvatarStore avatarStore,
-            final ProfileKeyProvider profileKeyProvider,
             final UnidentifiedAccessProvider unidentifiedAccessProvider,
             final SignalServiceAddressResolver addressResolver
     ) {
         this.account = account;
         this.dependencies = dependencies;
         this.avatarStore = avatarStore;
-        this.profileKeyProvider = profileKeyProvider;
         this.unidentifiedAccessProvider = unidentifiedAccessProvider;
         this.addressResolver = addressResolver;
     }
@@ -231,7 +229,8 @@ public final class ProfileHelper {
     }
 
     private SignalServiceProfile retrieveProfileSync(String username) throws IOException {
-        return dependencies.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
+        final var locale = Locale.getDefault();
+        return dependencies.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent(), locale);
     }
 
     private ProfileAndCredential retrieveProfileAndCredential(
@@ -296,7 +295,7 @@ public final class ProfileHelper {
             RecipientId recipientId, SignalServiceProfile.RequestType requestType
     ) {
         var unidentifiedAccess = getUnidentifiedAccess(recipientId);
-        var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId));
+        var profileKey = Optional.fromNullable(account.getProfileStore().getProfileKey(recipientId));
 
         final var address = addressResolver.resolveSignalServiceAddress(recipientId);
         return retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
@@ -311,11 +310,16 @@ public final class ProfileHelper {
         var profileService = dependencies.getProfileService();
 
         Single<ServiceResponse<ProfileAndCredential>> responseSingle;
+        final var locale = Locale.getDefault();
         try {
-            responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType);
+            responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType, locale);
         } catch (NoClassDefFoundError e) {
             // Native zkgroup lib not available for ProfileKey
-            responseSingle = profileService.getProfile(address, Optional.absent(), unidentifiedAccess, requestType);
+            responseSingle = profileService.getProfile(address,
+                    Optional.absent(),
+                    unidentifiedAccess,
+                    requestType,
+                    locale);
         }
 
         return responseSingle.map(pair -> {
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java
deleted file mode 100644 (file)
index b98d674..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.asamk.signal.manager.helper;
-
-import org.asamk.signal.manager.storage.recipients.RecipientId;
-import org.signal.zkgroup.profiles.ProfileKey;
-
-public interface ProfileKeyProvider {
-
-    ProfileKey getProfileKey(RecipientId address);
-}
index c0953f1fa27be3deb94f0a95fb3a13cb32bb4670..6c0fb2e9cbde4d3974679f00e7370d0317f6ab39 100644 (file)
@@ -100,7 +100,7 @@ public class SendHelper {
             final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
     ) throws IOException, GroupSendingNotAllowedException {
         GroupUtils.setGroupContext(messageBuilder, g);
-        messageBuilder.withExpiration(g.getMessageExpirationTime());
+        messageBuilder.withExpiration(g.getMessageExpirationTimer());
 
         final var message = messageBuilder.build();
         final var recipients = g.getMembersWithout(account.getSelfRecipientId());
index e3fc7fc26676e7f7e284ef5d80b1d75f4c8cc12f..6fe04dce12483bd92f7210747659589ab6220a2c 100644 (file)
@@ -83,8 +83,7 @@ public class SyncHelper {
             try (OutputStream fos = new FileOutputStream(groupsFile)) {
                 var out = new DeviceGroupsOutputStream(fos);
                 for (var record : account.getGroupStore().getGroups()) {
-                    if (record instanceof GroupInfoV1) {
-                        var groupInfo = (GroupInfoV1) record;
+                    if (record instanceof GroupInfoV1 groupInfo) {
                         out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
                                 Optional.fromNullable(groupInfo.name),
                                 groupInfo.getMembers()
index 87e23c1b33aea4782536d3be4840f6402afb2fba..661a7f96c0ed3f705cd97405b75a5a1ea2c2d73e 100644 (file)
@@ -1,11 +1,16 @@
 package org.asamk.signal.manager.helper;
 
+import org.asamk.signal.manager.SignalDependencies;
+import org.asamk.signal.manager.storage.SignalAccount;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -13,24 +18,39 @@ import static org.whispersystems.signalservice.internal.util.Util.getSecretBytes
 
 public class UnidentifiedAccessHelper {
 
-    private final SelfProfileKeyProvider selfProfileKeyProvider;
-
-    private final ProfileKeyProvider profileKeyProvider;
+    private final static Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
 
+    private final SignalAccount account;
+    private final SignalDependencies dependencies;
+    private final SelfProfileKeyProvider selfProfileKeyProvider;
     private final ProfileProvider profileProvider;
 
-    private final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider;
-
     public UnidentifiedAccessHelper(
+            final SignalAccount account,
+            final SignalDependencies dependencies,
             final SelfProfileKeyProvider selfProfileKeyProvider,
-            final ProfileKeyProvider profileKeyProvider,
-            final ProfileProvider profileProvider,
-            final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider
+            final ProfileProvider profileProvider
     ) {
+        this.account = account;
+        this.dependencies = dependencies;
         this.selfProfileKeyProvider = selfProfileKeyProvider;
-        this.profileKeyProvider = profileKeyProvider;
         this.profileProvider = profileProvider;
-        this.senderCertificateProvider = senderCertificateProvider;
+    }
+
+    private byte[] getSenderCertificate() {
+        byte[] certificate;
+        try {
+            if (account.isPhoneNumberShared()) {
+                certificate = dependencies.getAccountManager().getSenderCertificate();
+            } else {
+                certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
+            }
+        } catch (IOException e) {
+            logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
+            return null;
+        }
+        // TODO cache for a day
+        return certificate;
     }
 
     private byte[] getSelfUnidentifiedAccessKey() {
@@ -45,7 +65,7 @@ public class UnidentifiedAccessHelper {
 
         switch (targetProfile.getUnidentifiedAccessMode()) {
             case ENABLED:
-                var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
+                var theirProfileKey = account.getProfileStore().getProfileKey(recipient);
                 if (theirProfileKey == null) {
                     return null;
                 }
@@ -60,7 +80,7 @@ public class UnidentifiedAccessHelper {
 
     public Optional<UnidentifiedAccessPair> getAccessForSync() {
         var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
-        var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
+        var selfUnidentifiedAccessCertificate = getSenderCertificate();
 
         if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
             return Optional.absent();
@@ -82,7 +102,7 @@ public class UnidentifiedAccessHelper {
     public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) {
         var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
         var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
-        var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
+        var selfUnidentifiedAccessCertificate = getSenderCertificate();
 
         if (recipientUnidentifiedAccessKey == null
                 || selfUnidentifiedAccessKey == null
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java
deleted file mode 100644 (file)
index b059734..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.asamk.signal.manager.helper;
-
-public interface UnidentifiedAccessSenderCertificateProvider {
-
-    byte[] getSenderCertificate();
-}
index 60efc84b2df4baf54efef4abd66317f10d1a1481..2c2102595fb23c8b56f0e929c3c0ee49dfc2cca6 100644 (file)
@@ -2,13 +2,14 @@ package org.asamk.signal.manager.storage.groups;
 
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-public abstract class GroupInfo {
+public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
 
     public abstract GroupId getGroupId();
 
@@ -38,10 +39,16 @@ public abstract class GroupInfo {
 
     public abstract void setBlocked(boolean blocked);
 
-    public abstract int getMessageExpirationTime();
+    public abstract int getMessageExpirationTimer();
 
     public abstract boolean isAnnouncementGroup();
 
+    public abstract GroupPermission getPermissionAddMember();
+
+    public abstract GroupPermission getPermissionEditDetails();
+
+    public abstract GroupPermission getPermissionSendMessage();
+
     public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
         return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
     }
index 49c9a5042b34187fa17df91705a8dae71a3211f7..4e759e5fc32d5dce4c1c5c4ad250de0400af0b1c 100644 (file)
@@ -3,6 +3,7 @@ package org.asamk.signal.manager.storage.groups;
 import org.asamk.signal.manager.groups.GroupIdV1;
 import org.asamk.signal.manager.groups.GroupIdV2;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 
@@ -10,7 +11,7 @@ import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 
-public class GroupInfoV1 extends GroupInfo {
+public final class GroupInfoV1 extends GroupInfo {
 
     private final GroupIdV1 groupId;
 
@@ -85,7 +86,7 @@ public class GroupInfoV1 extends GroupInfo {
     }
 
     @Override
-    public int getMessageExpirationTime() {
+    public int getMessageExpirationTimer() {
         return messageExpirationTime;
     }
 
@@ -94,6 +95,21 @@ public class GroupInfoV1 extends GroupInfo {
         return false;
     }
 
+    @Override
+    public GroupPermission getPermissionAddMember() {
+        return GroupPermission.EVERY_MEMBER;
+    }
+
+    @Override
+    public GroupPermission getPermissionEditDetails() {
+        return GroupPermission.EVERY_MEMBER;
+    }
+
+    @Override
+    public GroupPermission getPermissionSendMessage() {
+        return GroupPermission.EVERY_MEMBER;
+    }
+
     public void addMembers(Collection<RecipientId> members) {
         this.members.addAll(members);
     }
index a06b83dfa3203d064533266429c1d92dcfac91ee..793cde5d9c81b5c35464d1937ffd8cb14b17f100 100644 (file)
@@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups;
 
 import org.asamk.signal.manager.groups.GroupIdV2;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
 import org.signal.storageservice.protos.groups.AccessControl;
@@ -14,7 +15,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-public class GroupInfoV2 extends GroupInfo {
+public final class GroupInfoV2 extends GroupInfo {
 
     private final GroupIdV2 groupId;
     private final GroupMasterKey masterKey;
@@ -151,7 +152,7 @@ public class GroupInfoV2 extends GroupInfo {
     }
 
     @Override
-    public int getMessageExpirationTime() {
+    public int getMessageExpirationTimer() {
         return this.group != null && this.group.hasDisappearingMessagesTimer()
                 ? this.group.getDisappearingMessagesTimer().getDuration()
                 : 0;
@@ -162,6 +163,23 @@ public class GroupInfoV2 extends GroupInfo {
         return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED;
     }
 
+    @Override
+    public GroupPermission getPermissionAddMember() {
+        final var accessControl = getAccessControl();
+        return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getMembers());
+    }
+
+    @Override
+    public GroupPermission getPermissionEditDetails() {
+        final var accessControl = getAccessControl();
+        return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getAttributes());
+    }
+
+    @Override
+    public GroupPermission getPermissionSendMessage() {
+        return isAnnouncementGroup() ? GroupPermission.ONLY_ADMINS : GroupPermission.EVERY_MEMBER;
+    }
+
     public void setPermissionDenied(final boolean permissionDenied) {
         this.permissionDenied = permissionDenied;
     }
@@ -169,4 +187,19 @@ public class GroupInfoV2 extends GroupInfo {
     public boolean isPermissionDenied() {
         return permissionDenied;
     }
+
+    private AccessControl getAccessControl() {
+        if (this.group == null || !this.group.hasAccessControl()) {
+            return null;
+        }
+
+        return this.group.getAccessControl();
+    }
+
+    private static GroupPermission toGroupPermission(final AccessControl.AccessRequired permission) {
+        return switch (permission) {
+            case ADMINISTRATOR -> GroupPermission.ONLY_ADMINS;
+            default -> GroupPermission.EVERY_MEMBER;
+        };
+    }
 }
index fe8f85a6740184ef49002803b15deadd57176740..cfa39bc9aaf4921477b3f351cb34671b7c17f42c 100644 (file)
@@ -30,6 +30,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Base64;
 import java.util.HashMap;
@@ -74,8 +75,7 @@ public class GroupStore {
             final Saver saver
     ) {
         final var groups = storage.groups.stream().map(g -> {
-            if (g instanceof Storage.GroupV1) {
-                final var g1 = (Storage.GroupV1) g;
+            if (g instanceof Storage.GroupV1 g1) {
                 final var members = g1.members.stream().map(m -> {
                     if (m.recipientId == null) {
                         return recipientResolver.resolveRecipient(new RecipientAddress(UuidUtil.parseOrNull(m.uuid),
@@ -122,7 +122,11 @@ public class GroupStore {
                     }
                     final var groupFileLegacy = getGroupV2FileLegacy(group.getGroupId());
                     if (groupFileLegacy.exists()) {
-                        groupFileLegacy.delete();
+                        try {
+                            Files.delete(groupFileLegacy.toPath());
+                        } catch (IOException e) {
+                            logger.error("Failed to delete legacy group file {}: {}", groupFileLegacy, e.getMessage());
+                        }
                     }
                 } catch (IOException e) {
                     logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
@@ -181,8 +185,7 @@ public class GroupStore {
         synchronized (groups) {
             var modified = false;
             for (var group : this.groups.values()) {
-                if (group instanceof GroupInfoV1) {
-                    var groupV1 = (GroupInfoV1) group;
+                if (group instanceof GroupInfoV1 groupV1) {
                     if (groupV1.isMember(toBeMergedRecipientId)) {
                         groupV1.removeMember(toBeMergedRecipientId);
                         groupV1.addMembers(List.of(recipientId));
@@ -215,8 +218,7 @@ public class GroupStore {
 
     private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) {
         for (var g : groups.values()) {
-            if (g instanceof GroupInfoV1) {
-                final var gv1 = (GroupInfoV1) g;
+            if (g instanceof GroupInfoV1 gv1) {
                 if (groupIdV2.equals(gv1.getExpectedV2Id())) {
                     return gv1;
                 }
@@ -251,8 +253,7 @@ public class GroupStore {
 
     private Storage toStorageLocked() {
         return new Storage(groups.values().stream().map(g -> {
-            if (g instanceof GroupInfoV1) {
-                final var g1 = (GroupInfoV1) g;
+            if (g instanceof GroupInfoV1 g1) {
                 return new Storage.GroupV1(g1.getGroupId().toBase64(),
                         g1.getExpectedV2Id().toBase64(),
                         g1.name,
@@ -261,7 +262,7 @@ public class GroupStore {
                         g1.blocked,
                         g1.archived,
                         g1.members.stream()
-                                .map(m -> new Storage.GroupV1.Member(m.getId(), null, null))
+                                .map(m -> new Storage.GroupV1.Member(m.id(), null, null))
                                 .collect(Collectors.toList()));
             }
 
@@ -273,91 +274,22 @@ public class GroupStore {
         }).collect(Collectors.toList()));
     }
 
-    public static class Storage {
-
-        @JsonDeserialize(using = GroupsDeserializer.class)
-        public List<Storage.Group> groups;
-
-        // For deserialization
-        public Storage() {
-        }
-
-        public Storage(final List<Storage.Group> groups) {
-            this.groups = groups;
-        }
-
-        private abstract static class Group {
-
-        }
-
-        private static class GroupV1 extends Group {
-
-            public String groupId;
-            public String expectedV2Id;
-            public String name;
-            public String color;
-            public int messageExpirationTime;
-            public boolean blocked;
-            public boolean archived;
+    public record Storage(@JsonDeserialize(using = GroupsDeserializer.class) List<Object> groups) {
 
-            @JsonDeserialize(using = MembersDeserializer.class)
-            @JsonSerialize(using = MembersSerializer.class)
-            public List<Member> members;
+        private record GroupV1(
+                String groupId,
+                String expectedV2Id,
+                String name,
+                String color,
+                int messageExpirationTime,
+                boolean blocked,
+                boolean archived,
+                @JsonDeserialize(using = MembersDeserializer.class) @JsonSerialize(using = MembersSerializer.class) List<Member> members
+        ) {
 
-            // For deserialization
-            public GroupV1() {
-            }
-
-            public GroupV1(
-                    final String groupId,
-                    final String expectedV2Id,
-                    final String name,
-                    final String color,
-                    final int messageExpirationTime,
-                    final boolean blocked,
-                    final boolean archived,
-                    final List<Member> members
-            ) {
-                this.groupId = groupId;
-                this.expectedV2Id = expectedV2Id;
-                this.name = name;
-                this.color = color;
-                this.messageExpirationTime = messageExpirationTime;
-                this.blocked = blocked;
-                this.archived = archived;
-                this.members = members;
-            }
+            private record Member(Long recipientId, String uuid, String number) {}
 
-            private static final class Member {
-
-                public Long recipientId;
-
-                public String uuid;
-
-                public String number;
-
-                Member(Long recipientId, final String uuid, final String number) {
-                    this.recipientId = recipientId;
-                    this.uuid = uuid;
-                    this.number = number;
-                }
-            }
-
-            private static final class JsonRecipientAddress {
-
-                public String uuid;
-
-                public String number;
-
-                // For deserialization
-                public JsonRecipientAddress() {
-                }
-
-                JsonRecipientAddress(final String uuid, final String number) {
-                    this.uuid = uuid;
-                    this.number = number;
-                }
-            }
+            private record JsonRecipientAddress(String uuid, String number) {}
 
             private static class MembersSerializer extends JsonSerializer<List<Member>> {
 
@@ -365,7 +297,7 @@ public class GroupStore {
                 public void serialize(
                         final List<Member> value, final JsonGenerator jgen, final SerializerProvider provider
                 ) throws IOException {
-                    jgen.writeStartArray(value.size());
+                    jgen.writeStartArray(null, value.size());
                     for (var address : value) {
                         if (address.recipientId != null) {
                             jgen.writeNumber(address.recipientId);
@@ -403,39 +335,19 @@ public class GroupStore {
             }
         }
 
-        private static class GroupV2 extends Group {
-
-            public String groupId;
-            public String masterKey;
-            public boolean blocked;
-            public boolean permissionDenied;
-
-            // For deserialization
-            private GroupV2() {
-            }
-
-            public GroupV2(
-                    final String groupId, final String masterKey, final boolean blocked, final boolean permissionDenied
-            ) {
-                this.groupId = groupId;
-                this.masterKey = masterKey;
-                this.blocked = blocked;
-                this.permissionDenied = permissionDenied;
-            }
-        }
-
+        private record GroupV2(String groupId, String masterKey, boolean blocked, boolean permissionDenied) {}
     }
 
-    private static class GroupsDeserializer extends JsonDeserializer<List<Storage.Group>> {
+    private static class GroupsDeserializer extends JsonDeserializer<List<Object>> {
 
         @Override
-        public List<Storage.Group> deserialize(
+        public List<Object> deserialize(
                 JsonParser jsonParser, DeserializationContext deserializationContext
         ) throws IOException {
-            var groups = new ArrayList<Storage.Group>();
+            var groups = new ArrayList<>();
             JsonNode node = jsonParser.getCodec().readTree(jsonParser);
             for (var n : node) {
-                Storage.Group g;
+                Object g;
                 if (n.hasNonNull("masterKey")) {
                     // a v2 group
                     g = jsonParser.getCodec().treeToValue(n, Storage.GroupV2.class);
index f24e77b1ef2293ba61cbc3b25d0095a4be6e5d90..fdea8b58d210c97fd8040696a0f5f542c65767c1 100644 (file)
@@ -185,7 +185,7 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden
         } catch (IOException e) {
             throw new AssertionError("Failed to create identities path", e);
         }
-        return new File(identitiesPath, String.valueOf(recipientId.getId()));
+        return new File(identitiesPath, String.valueOf(recipientId.id()));
     }
 
     private IdentityInfo loadIdentityLocked(final RecipientId recipientId) {
@@ -203,9 +203,9 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden
         try (var inputStream = new FileInputStream(file)) {
             var storage = objectMapper.readValue(inputStream, IdentityStorage.class);
 
-            var id = new IdentityKey(Base64.getDecoder().decode(storage.getIdentityKey()));
-            var trustLevel = TrustLevel.fromInt(storage.getTrustLevel());
-            var added = new Date(storage.getAddedTimestamp());
+            var id = new IdentityKey(Base64.getDecoder().decode(storage.identityKey()));
+            var trustLevel = TrustLevel.fromInt(storage.trustLevel());
+            var added = new Date(storage.addedTimestamp());
 
             final var identityInfo = new IdentityInfo(recipientId, id, trustLevel, added);
             cachedIdentities.put(recipientId, identityInfo);
@@ -251,32 +251,5 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden
         }
     }
 
-    private static final class IdentityStorage {
-
-        private String identityKey;
-        private int trustLevel;
-        private long addedTimestamp;
-
-        // For deserialization
-        private IdentityStorage() {
-        }
-
-        private IdentityStorage(final String identityKey, final int trustLevel, final long addedTimestamp) {
-            this.identityKey = identityKey;
-            this.trustLevel = trustLevel;
-            this.addedTimestamp = addedTimestamp;
-        }
-
-        public String getIdentityKey() {
-            return identityKey;
-        }
-
-        public int getTrustLevel() {
-            return trustLevel;
-        }
-
-        public long getAddedTimestamp() {
-            return addedTimestamp;
-        }
-    }
+    private record IdentityStorage(String identityKey, int trustLevel, long addedTimestamp) {}
 }
index fc5172782324c42932279ae85bc1a879e595e0ca..0ba0a8d5a338368177ef8b4c3331e7a0765ee51d 100644 (file)
@@ -76,7 +76,7 @@ public class MessageCache {
             return messageCachePath;
         }
 
-        var sender = String.valueOf(recipientId.getId());
+        var sender = String.valueOf(recipientId.id());
         return new File(messageCachePath, sender.replace("/", "_"));
     }
 
index c0f5b0b8d1cb90dd35102e6859060e5152fcde1f..bd8710dd1dad0e44465fae7c6f331890d7fc5e68 100644 (file)
@@ -19,7 +19,7 @@ public class RecipientAddress {
      */
     public RecipientAddress(Optional<UUID> uuid, Optional<String> e164) {
         uuid = uuid.isPresent() && uuid.get().equals(UuidUtil.UNKNOWN_UUID) ? Optional.empty() : uuid;
-        if (!uuid.isPresent() && !e164.isPresent()) {
+        if (uuid.isEmpty() && e164.isEmpty()) {
             throw new AssertionError("Must have either a UUID or E164 number!");
         }
 
index f093ca334515a517ebca3a6ae6ac8950d7f44a28..d7aa373dc6df0da85d450eb111e63a502697dddb 100644 (file)
@@ -1,38 +1,8 @@
 package org.asamk.signal.manager.storage.recipients;
 
-public class RecipientId {
-
-    private final long id;
-
-    RecipientId(final long id) {
-        this.id = id;
-    }
+public record RecipientId(long id) {
 
     public static RecipientId of(long id) {
         return new RecipientId(id);
     }
-
-    public long getId() {
-        return id;
-    }
-
-    @Override
-    public String toString() {
-        return "RecipientId{" + "id=" + id + '}';
-    }
-
-    @Override
-    public boolean equals(final Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        final RecipientId that = (RecipientId) o;
-
-        return id == that.id;
-    }
-
-    @Override
-    public int hashCode() {
-        return (int) (id ^ (id >>> 32));
-    }
 }
index 1630269270714e3672b7b3a829e3be520fa8a69c..f63e3b6cf27516d11d98b1511e801de81207d692 100644 (file)
@@ -453,7 +453,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
                                     .stream()
                                     .map(Enum::name)
                                     .collect(Collectors.toSet()));
-            return new Storage.Recipient(pair.getKey().getId(),
+            return new Storage.Recipient(pair.getKey().id(),
                     recipient.getAddress().getNumber().orElse(null),
                     recipient.getAddress().getUuid().map(UUID::toString).orElse(null),
                     recipient.getProfileKey() == null
@@ -479,115 +479,32 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
         }
     }
 
-    private static class Storage {
-
-        public List<Recipient> recipients;
-
-        public long lastId;
-
-        // For deserialization
-        private Storage() {
-        }
-
-        public Storage(final List<Recipient> recipients, final long lastId) {
-            this.recipients = recipients;
-            this.lastId = lastId;
-        }
-
-        private static class Recipient {
-
-            public long id;
-            public String number;
-            public String uuid;
-            public String profileKey;
-            public String profileKeyCredential;
-            public Contact contact;
-            public Profile profile;
-
-            // For deserialization
-            private Recipient() {
-            }
-
-            public Recipient(
-                    final long id,
-                    final String number,
-                    final String uuid,
-                    final String profileKey,
-                    final String profileKeyCredential,
-                    final Contact contact,
-                    final Profile profile
-            ) {
-                this.id = id;
-                this.number = number;
-                this.uuid = uuid;
-                this.profileKey = profileKey;
-                this.profileKeyCredential = profileKeyCredential;
-                this.contact = contact;
-                this.profile = profile;
-            }
-
-            private static class Contact {
-
-                public String name;
-                public String color;
-                public int messageExpirationTime;
-                public boolean blocked;
-                public boolean archived;
-
-                // For deserialization
-                public Contact() {
-                }
-
-                public Contact(
-                        final String name,
-                        final String color,
-                        final int messageExpirationTime,
-                        final boolean blocked,
-                        final boolean archived
-                ) {
-                    this.name = name;
-                    this.color = color;
-                    this.messageExpirationTime = messageExpirationTime;
-                    this.blocked = blocked;
-                    this.archived = archived;
-                }
-            }
-
-            private static class Profile {
-
-                public long lastUpdateTimestamp;
-                public String givenName;
-                public String familyName;
-                public String about;
-                public String aboutEmoji;
-                public String avatarUrlPath;
-                public String unidentifiedAccessMode;
-                public Set<String> capabilities;
-
-                // For deserialization
-                private Profile() {
-                }
-
-                public Profile(
-                        final long lastUpdateTimestamp,
-                        final String givenName,
-                        final String familyName,
-                        final String about,
-                        final String aboutEmoji,
-                        final String avatarUrlPath,
-                        final String unidentifiedAccessMode,
-                        final Set<String> capabilities
-                ) {
-                    this.lastUpdateTimestamp = lastUpdateTimestamp;
-                    this.givenName = givenName;
-                    this.familyName = familyName;
-                    this.about = about;
-                    this.aboutEmoji = aboutEmoji;
-                    this.avatarUrlPath = avatarUrlPath;
-                    this.unidentifiedAccessMode = unidentifiedAccessMode;
-                    this.capabilities = capabilities;
-                }
-            }
+    private record Storage(List<Recipient> recipients, long lastId) {
+
+        private record Recipient(
+                long id,
+                String number,
+                String uuid,
+                String profileKey,
+                String profileKeyCredential,
+                Storage.Recipient.Contact contact,
+                Storage.Recipient.Profile profile
+        ) {
+
+            private record Contact(
+                    String name, String color, int messageExpirationTime, boolean blocked, boolean archived
+            ) {}
+
+            private record Profile(
+                    long lastUpdateTimestamp,
+                    String givenName,
+                    String familyName,
+                    String about,
+                    String aboutEmoji,
+                    String avatarUrlPath,
+                    String unidentifiedAccessMode,
+                    Set<String> capabilities
+            ) {}
         }
     }
 
index f84903e44765fd011a3f4b2ef78ae693157b8e79..fb26fa007f532d7f0e199bbb903afd1fb2175129 100644 (file)
@@ -95,7 +95,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
                 return;
             }
 
-            logger.debug("Only to be merged recipient had sender keys, re-assigning to the new recipient.");
+            logger.debug("To be merged recipient had sender keys, re-assigning to the new recipient.");
             for (var key : keys) {
                 final var toBeMergedSenderKey = loadSenderKeyLocked(key);
                 deleteSenderKeyLocked(key);
@@ -103,12 +103,12 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
                     continue;
                 }
 
-                final var newKey = new Key(recipientId, key.getDeviceId(), key.distributionId);
+                final var newKey = new Key(recipientId, key.deviceId(), key.distributionId);
                 final var senderKeyRecord = loadSenderKeyLocked(newKey);
                 if (senderKeyRecord != null) {
                     continue;
                 }
-                storeSenderKeyLocked(newKey, senderKeyRecord);
+                storeSenderKeyLocked(newKey, toBeMergedSenderKey);
             }
         }
     }
@@ -126,7 +126,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
     }
 
     private List<Key> getKeysLocked(RecipientId recipientId) {
-        final var files = senderKeysPath.listFiles((_file, s) -> s.startsWith(recipientId.getId() + "_"));
+        final var files = senderKeysPath.listFiles((_file, s) -> s.startsWith(recipientId.id() + "_"));
         if (files == null) {
             return List.of();
         }
@@ -152,7 +152,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
             throw new AssertionError("Failed to create sender keys path", e);
         }
         return new File(senderKeysPath,
-                key.getRecipientId().getId() + "_" + key.getDeviceId() + "_" + key.distributionId.toString());
+                key.recipientId().id() + "_" + key.deviceId() + "_" + key.distributionId.toString());
     }
 
     private SenderKeyRecord loadSenderKeyLocked(final Key key) {
@@ -212,50 +212,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
         }
     }
 
-    private static final class Key {
+    private record Key(RecipientId recipientId, int deviceId, UUID distributionId) {
 
-        private final RecipientId recipientId;
-        private final int deviceId;
-        private final UUID distributionId;
-
-        public Key(
-                final RecipientId recipientId, final int deviceId, final UUID distributionId
-        ) {
-            this.recipientId = recipientId;
-            this.deviceId = deviceId;
-            this.distributionId = distributionId;
-        }
-
-        public RecipientId getRecipientId() {
-            return recipientId;
-        }
-
-        public int getDeviceId() {
-            return deviceId;
-        }
-
-        public UUID getDistributionId() {
-            return distributionId;
-        }
-
-        @Override
-        public boolean equals(final Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-
-            final Key key = (Key) o;
-
-            if (deviceId != key.deviceId) return false;
-            if (!recipientId.equals(key.recipientId)) return false;
-            return distributionId.equals(key.distributionId);
-        }
-
-        @Override
-        public int hashCode() {
-            int result = recipientId.hashCode();
-            result = 31 * result + deviceId;
-            result = 31 * result + distributionId.hashCode();
-            return result;
-        }
     }
 }
index 3faf2e74acf5fe1d37acaee4748548e9468a39f2..08197aec1e51f02d453289f31a9e373a1150f55f 100644 (file)
@@ -87,8 +87,8 @@ public class SenderKeySharedStore {
         synchronized (sharedSenderKeys) {
             return sharedSenderKeys.get(distributionId)
                     .stream()
-                    .map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.getRecipientId())
-                            .getIdentifier(), k.getDeviceId()))
+                    .map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.recipientId())
+                            .getIdentifier(), k.deviceId()))
                     .collect(Collectors.toSet());
         }
     }
@@ -146,7 +146,7 @@ public class SenderKeySharedStore {
 
                 sharedSenderKeys.put(distributionId, new HashSet<>(entries) {
                     {
-                        entries.removeIf(e -> e.getRecipientId().equals(recipientId));
+                        removeIf(e -> e.recipientId().equals(recipientId));
                     }
                 });
             }
@@ -163,7 +163,7 @@ public class SenderKeySharedStore {
                         entries.stream()
                                 .map(e -> e.recipientId.equals(toBeMergedRecipientId) ? new SenderKeySharedEntry(
                                         recipientId,
-                                        e.getDeviceId()) : e)
+                                        e.deviceId()) : e)
                                 .collect(Collectors.toSet()));
             }
             saveLocked();
@@ -181,8 +181,8 @@ public class SenderKeySharedStore {
         var storage = new Storage(sharedSenderKeys.entrySet().stream().flatMap(pair -> {
             final var sharedWith = pair.getValue();
             return sharedWith.stream()
-                    .map(entry -> new Storage.SharedSenderKey(entry.getRecipientId().getId(),
-                            entry.getDeviceId(),
+                    .map(entry -> new Storage.SharedSenderKey(entry.recipientId().id(),
+                            entry.deviceId(),
                             pair.getKey().asUuid().toString()));
         }).collect(Collectors.toList()));
 
@@ -199,72 +199,10 @@ public class SenderKeySharedStore {
         }
     }
 
-    private static class Storage {
+    private record Storage(List<SharedSenderKey> sharedSenderKeys) {
 
-        public List<SharedSenderKey> sharedSenderKeys;
-
-        // For deserialization
-        private Storage() {
-        }
-
-        public Storage(final List<SharedSenderKey> sharedSenderKeys) {
-            this.sharedSenderKeys = sharedSenderKeys;
-        }
-
-        private static class SharedSenderKey {
-
-            public long recipientId;
-            public int deviceId;
-            public String distributionId;
-
-            // For deserialization
-            private SharedSenderKey() {
-            }
-
-            public SharedSenderKey(final long recipientId, final int deviceId, final String distributionId) {
-                this.recipientId = recipientId;
-                this.deviceId = deviceId;
-                this.distributionId = distributionId;
-            }
-        }
+        private record SharedSenderKey(long recipientId, int deviceId, String distributionId) {}
     }
 
-    private static final class SenderKeySharedEntry {
-
-        private final RecipientId recipientId;
-        private final int deviceId;
-
-        public SenderKeySharedEntry(
-                final RecipientId recipientId, final int deviceId
-        ) {
-            this.recipientId = recipientId;
-            this.deviceId = deviceId;
-        }
-
-        public RecipientId getRecipientId() {
-            return recipientId;
-        }
-
-        public int getDeviceId() {
-            return deviceId;
-        }
-
-        @Override
-        public boolean equals(final Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-
-            final SenderKeySharedEntry that = (SenderKeySharedEntry) o;
-
-            if (deviceId != that.deviceId) return false;
-            return recipientId.equals(that.recipientId);
-        }
-
-        @Override
-        public int hashCode() {
-            int result = recipientId.hashCode();
-            result = 31 * result + deviceId;
-            return result;
-        }
-    }
+    private record SenderKeySharedEntry(RecipientId recipientId, int deviceId) {}
 }
index bae4fdf31271557778981c4667d0e63a3443bfbb..627ef45ec270656a35545f409b92854a8bbf62e6 100644 (file)
@@ -88,8 +88,8 @@ public class SessionStore implements SignalServiceSessionStore {
         synchronized (cachedSessions) {
             return getKeysLocked(recipientId).stream()
                     // get all sessions for recipient except main device session
-                    .filter(key -> key.getDeviceId() != 1 && key.getRecipientId().equals(recipientId))
-                    .map(Key::getDeviceId)
+                    .filter(key -> key.deviceId() != 1 && key.recipientId().equals(recipientId))
+                    .map(Key::deviceId)
                     .collect(Collectors.toList());
         }
     }
@@ -155,7 +155,7 @@ public class SessionStore implements SignalServiceSessionStore {
                     .stream()
                     .flatMap(recipientId -> getKeysLocked(recipientId).stream())
                     .filter(key -> isActive(this.loadSessionLocked(key)))
-                    .map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId), key.getDeviceId()))
+                    .map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId), key.deviceId()))
                     .collect(Collectors.toSet());
         }
     }
@@ -197,7 +197,7 @@ public class SessionStore implements SignalServiceSessionStore {
                     if (session == null) {
                         continue;
                     }
-                    final var newKey = new Key(recipientId, key.getDeviceId());
+                    final var newKey = new Key(recipientId, key.deviceId());
                     storeSessionLocked(newKey, session);
                 }
             }
@@ -217,7 +217,7 @@ public class SessionStore implements SignalServiceSessionStore {
     }
 
     private List<Key> getKeysLocked(RecipientId recipientId) {
-        final var files = sessionsPath.listFiles((_file, s) -> s.startsWith(recipientId.getId() + "_"));
+        final var files = sessionsPath.listFiles((_file, s) -> s.startsWith(recipientId.id() + "_"));
         if (files == null) {
             return List.of();
         }
@@ -249,7 +249,7 @@ public class SessionStore implements SignalServiceSessionStore {
         } catch (IOException e) {
             throw new AssertionError("Failed to create sessions path", e);
         }
-        return new File(sessionsPath, key.getRecipientId().getId() + "_" + key.getDeviceId());
+        return new File(sessionsPath, key.recipientId().id() + "_" + key.deviceId());
     }
 
     private SessionRecord loadSessionLocked(final Key key) {
@@ -324,40 +324,5 @@ public class SessionStore implements SignalServiceSessionStore {
                 && record.getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
     }
 
-    private static final class Key {
-
-        private final RecipientId recipientId;
-        private final int deviceId;
-
-        public Key(final RecipientId recipientId, final int deviceId) {
-            this.recipientId = recipientId;
-            this.deviceId = deviceId;
-        }
-
-        public RecipientId getRecipientId() {
-            return recipientId;
-        }
-
-        public int getDeviceId() {
-            return deviceId;
-        }
-
-        @Override
-        public boolean equals(final Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-
-            final var key = (Key) o;
-
-            if (deviceId != key.deviceId) return false;
-            return recipientId.equals(key.recipientId);
-        }
-
-        @Override
-        public int hashCode() {
-            int result = recipientId.hashCode();
-            result = 31 * result + deviceId;
-            return result;
-        }
-    }
+    private record Key(RecipientId recipientId, int deviceId) {}
 }
index 19803bdc64ec0f36255f807d36440fac8047bb57..d0c9c5bc15831b72f60657204ce3c51590edeaca 100644 (file)
@@ -65,33 +65,10 @@ public class StickerStore {
                 .collect(Collectors.toList()));
     }
 
-    public static class Storage {
+    public record Storage(List<Storage.Sticker> stickers) {
 
-        public List<Storage.Sticker> stickers;
+        private record Sticker(String packId, String packKey, boolean installed) {
 
-        // For deserialization
-        private Storage() {
-        }
-
-        public Storage(final List<Sticker> stickers) {
-            this.stickers = stickers;
-        }
-
-        private static class Sticker {
-
-            public String packId;
-            public String packKey;
-            public boolean installed;
-
-            // For deserialization
-            private Sticker() {
-            }
-
-            public Sticker(final String packId, final String packKey, final boolean installed) {
-                this.packId = packId;
-                this.packKey = packKey;
-                this.installed = installed;
-            }
         }
     }
 
index 66b1429677acd4d329482bc39107f2ffd9a01f14..ed94f39e609044258fead499ec09f30016d463a6 100644 (file)
@@ -59,7 +59,7 @@ public class MessageCacheUtils {
             if (version >= 4) {
                 serverDeliveredTimestamp = in.readLong();
             }
-            Optional<SignalServiceAddress> addressOptional = sourceUuid == null && source.isEmpty()
+            Optional<SignalServiceAddress> addressOptional = sourceUuid == null
                     ? Optional.absent()
                     : Optional.of(new SignalServiceAddress(sourceUuid, source));
             return new SignalServiceEnvelope(type,
index c1b1183c31bc3454ce96b5f5b3f864717d632fb9..0f5b7407ca94fca7a8deed3918327bcc7a861c8e 100644 (file)
@@ -91,14 +91,11 @@ public class ProfileUtils {
         }
         String[] parts = name.split("\0");
 
-        switch (parts.length) {
-            case 0:
-                return new Pair<>(null, null);
-            case 1:
-                return new Pair<>(parts[0], null);
-            default:
-                return new Pair<>(parts[0], parts[1]);
-        }
+        return switch (parts.length) {
+            case 0 -> new Pair<>(null, null);
+            case 1 -> new Pair<>(parts[0], null);
+            default -> new Pair<>(parts[0], parts[1]);
+        };
     }
 
     static String trimZeros(String str) {
index 4c7fd58e58b737617ea2f19d55c39cacaab4bf09..f332bbb84c4919fa17cf993cbe3375921461e438 100644 (file)
@@ -35,60 +35,59 @@ public class StickerUtils {
 
         var pack = parseStickerPack(rootPath, zip);
 
-        if (pack.stickers == null) {
+        if (pack.stickers() == null) {
             throw new StickerPackInvalidException("Must set a 'stickers' field.");
         }
 
-        if (pack.stickers.isEmpty()) {
+        if (pack.stickers().isEmpty()) {
             throw new StickerPackInvalidException("Must include stickers.");
         }
 
-        var stickers = new ArrayList<SignalServiceStickerManifestUpload.StickerInfo>(pack.stickers.size());
-        for (var sticker : pack.stickers) {
-            if (sticker.file == null) {
+        var stickers = new ArrayList<SignalServiceStickerManifestUpload.StickerInfo>(pack.stickers().size());
+        for (var sticker : pack.stickers()) {
+            if (sticker.file() == null) {
                 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
             }
 
             Pair<InputStream, Long> data;
             try {
-                data = getInputStreamAndLength(rootPath, zip, sticker.file);
+                data = getInputStreamAndLength(rootPath, zip, sticker.file());
             } catch (IOException ignored) {
-                throw new StickerPackInvalidException("Could not find find " + sticker.file);
+                throw new StickerPackInvalidException("Could not find find " + sticker.file());
             }
 
-            var contentType = sticker.contentType != null && !sticker.contentType.isEmpty()
-                    ? sticker.contentType
-                    : getContentType(rootPath, zip, sticker.file);
+            var contentType = sticker.contentType() != null && !sticker.contentType().isEmpty()
+                    ? sticker.contentType()
+                    : getContentType(rootPath, zip, sticker.file());
             var stickerInfo = new SignalServiceStickerManifestUpload.StickerInfo(data.first(),
                     data.second(),
-                    Optional.fromNullable(sticker.emoji).or(""),
+                    Optional.fromNullable(sticker.emoji()).or(""),
                     contentType);
             stickers.add(stickerInfo);
         }
 
         SignalServiceStickerManifestUpload.StickerInfo cover = null;
-        if (pack.cover != null) {
-            if (pack.cover.file == null) {
+        if (pack.cover() != null) {
+            if (pack.cover().file() == null) {
                 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
             }
 
             Pair<InputStream, Long> data;
             try {
-                data = getInputStreamAndLength(rootPath, zip, pack.cover.file);
+                data = getInputStreamAndLength(rootPath, zip, pack.cover().file());
             } catch (IOException ignored) {
-                throw new StickerPackInvalidException("Could not find find " + pack.cover.file);
+                throw new StickerPackInvalidException("Could not find find " + pack.cover().file());
             }
 
-            var contentType = pack.cover.contentType != null && !pack.cover.contentType.isEmpty()
-                    ? pack.cover.contentType
-                    : getContentType(rootPath, zip, pack.cover.file);
+            var contentType = pack.cover().contentType() != null && !pack.cover().contentType().isEmpty() ? pack.cover()
+                    .contentType() : getContentType(rootPath, zip, pack.cover().file());
             cover = new SignalServiceStickerManifestUpload.StickerInfo(data.first(),
                     data.second(),
-                    Optional.fromNullable(pack.cover.emoji).or(""),
+                    Optional.fromNullable(pack.cover().emoji()).or(""),
                     contentType);
         }
 
-        return new SignalServiceStickerManifestUpload(pack.title, pack.author, cover, stickers);
+        return new SignalServiceStickerManifestUpload(pack.title(), pack.author(), cover, stickers);
     }
 
     private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException {
index 093f5807a70316493726a36839dadaa2338cbaac..1bc0b879fd201928c646d684b6d3bd8b03895488 100755 (executable)
@@ -29,15 +29,18 @@ method(arg1<type>, arg2<type>, ...) -> return<type>
 
 Where <type> is according to DBus specification:
 
-* <s>   : String
-* <ay>  : Byte Array
-* <aay> : Array of Byte Arrays
-* <as>  : String Array
-* <ax>  : Array of signed 64 bit integer
-* <b>   : Boolean (0|1)
-* <x>   : Signed 64 bit integer
+* <a>   : Array of ... (comma-separated list) (array:)
+* (...) : Struct (cannot be sent via `dbus-send`)
+* <b>   : Boolean (false|true) (boolean:)
+* <i>   : Signed 32-bit (int) integer (int32:)
+* <o>   : DBusPath object (objpath:)
+* <s>   : String (string:)
+* <x>   : Signed 64-bit (long) integer (int64:)
+* <y>   : 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 +<countrycode><regional number>
@@ -45,9 +48,9 @@ Phone numbers always have the format +<countrycode><regional number>
 == 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<s>::
@@ -55,12 +58,12 @@ link(newDeviceName<s>) -> deviceLinkUri<s>::
 * 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<as>::
 * accountList : Array of all attached accounts in DBus object path form
@@ -86,94 +89,243 @@ verify(number<s>, verificationCode<s>) -> <>::
 
 Command fails if PIN was set after previous registration; use verifyWithPin instead.
 
-Exception: Failure, InvalidNumber
+Exceptions: Failure, InvalidNumber
 
 verifyWithPin(number<s>, verificationCode<s>, pin<s>) -> <>::
 * 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<s>::
 * 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<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
-* 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<s>, members<as>, avatar<s>) -> groupId<ay>::
+* 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<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
-updateProfile(givenName<s>, familyName<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
-* 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<ay>) -> objectPath<o>::
+* groupId    : Byte array representing the internal group identifier
+* objectPath : DBusPath for the group
+
+getGroupMembers(groupId<ay>) -> members<as>::
+* groupId   : Byte array representing the internal group identifier
+* members   : String array with the phone numbers of all active members of a group
+
+Exceptions: None, if the group name is not found an empty array is returned
+
+joinGroup(inviteURI<s>) -> <>::
+* 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<a(oays)>::
+* 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<s>, expiration<i>) -> <>::
-* 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<s>, attachments<as>, groupId<ay>) -> timestamp<x>::
+* 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<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
+* 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<x>, groupId<ay>) -> timestamp<x>::
+* 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<ay> (read-only)                : Byte array representing the internal group identifier
+* Name<s>                           : Display name of the group
+* Description<s>                    : Description of the group
+* Avatar<s> (write-only)            : Filename of the avatar
+* IsBlocked<b>                      : true=member will not receive group messages; false=not blocked
+* IsMember<b> (read-only)           : always true (object path exists only for group members)
+* IsAdmin<b> (read-only)            : true=member has admin privileges; false=not admin
+* MessageExpirationTimer<i>         : int32 representing message expiration time for group
+* Members<as> (read-only)           : String array of group members' phone numbers
+* PendingMembers<as> (read-only)    : String array of pending members' phone numbers
+* RequestingMembers<as> (read-only) : String array of requesting members' phone numbers
+* Admins<as> (read-only)            : String array of admins' phone numbers
+* PermissionAddMember<s>            : String representing who has permission to add members
+** ONLY_ADMINS, EVERY_MEMBER
+* PermissionEditDetails<s>          : String representing who may edit group details
+** ONLY_ADMINS, EVERY_MEMBER
+* PermissionSendMessage<s>          : String representing who post messages to group
+** ONLY_ADMINS, EVERY_MEMBER (note that ONLY_ADMINS is equivalent to IsAnnouncementGroup)
+* GroupInviteLink<s> (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<as>) -> <>::
+* recipients  : String array of phone numbers
+
+Grant admin privileges to recipients.
 
 Exceptions: Failure
 
-setContactBlocked(number<s>, block<b>) -> <>::
-* number  : Phone number affected by method
-* block   : 0=remove block , 1=blocked
+addMembers(recipients<as>) -> <>::
+* 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<ay>, block<b>) -> <>::
-* 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<s>) -> <>::
-* inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App
+enableLink(requiresApproval<b>) -> <>::
+* 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<as>) -> <>::
+* recipients  : String array of phone numbers
+
+Remove admin privileges from recipients.
+
+Exceptions: Failure
+
+removeMembers(recipients<as>) -> <>::
+* 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<aay>::
+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<ay>) -> groupName<s>::
+* 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<ay>) -> isGroupBlocked<b>::
+* 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<ay>) -> isMember<b>::
+* 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<ay>) -> <>::
 * groupId : Byte array representing the internal group identifier
 
 Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember()
 
-Exceptions: GroupNotFound, Failure
+Exceptions: GroupNotFound, Failure, InvalidGroupId
 
-isMember(groupId<ay>) -> active<b>::
+setGroupBlocked(groupId<ay>, block<b>) -> <>::
 * 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<as>) -> <>::
-* recipients : Array of phone numbers 
+Exceptions: GroupNotFound, InvalidGroupId
 
-Exceptions: Failure, InvalidNumber, UntrustedIdentity
+updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
+* 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<s>, attachments<as>, groupId<ay>) -> timestamp<x>::
-* 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<s>) -> <>::
+* 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<x>) -> devicePath<o>::
+* deviceId   : Long representing a deviceId
+* devicePath : DBusPath object for the device
+
+Exceptions: DeviceNotFound
+
+listDevices() -> devices<a(oxs)>::
+* 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 @@ 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<s>, attachments<as>) -> timestamp<x>::
-* 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<x> (read-only)       : Long representing the device identifier
+* Created<x> (read-only)  : Long representing the number of milliseconds since the Unix epoch
+* LastSeen<x> (read-only) : Long representing the number of milliseconds since the Unix epoch
+* Name<s>                 : String representing the display name of the device
 
-sendMessage(message<s>, attachments<as>, recipient<s>) -> timestamp<x>::
-sendMessage(message<s>, attachments<as>, recipients<as>) -> timestamp<x>::
-* 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<s>, stop<b>) -> <>::
-* recipient             : Phone number of a single recipient
-* targetSentTimestamp   : True, if typing state should be stopped
+removeDevice() -> <>::
 
-Exceptions: Failure, GroupNotFound, UntrustedIdentity
+Exceptions: Failure
 
-sendReadReceipt(recipient<s>, targetSentTimestamp<ax>) -> <>::
-* recipient             : Phone number of a single recipient
-* targetSentTimestamp   : Array of Longs to identify the corresponding signal messages
+=== Other methods
 
-Exceptions: Failure, UntrustedIdentity
+getContactName(number<s>) -> name<s>::
+* 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<s>, targetSentTimestamp<ax>) -> <>::
-* recipient             : Phone number of a single recipient
-* targetSentTimestamp   : Array of Longs to identify the corresponding signal messages
+Exceptions: None
 
-Exceptions: Failure, UntrustedIdentity
+getContactNumber(name<s>) -> numbers<as>::
+* numbers : Array of phone number
+* name    : Contact or profile name ("firstname lastname")
 
-sendGroupMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
-* 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<s>::
+* number : Your phone number
+
+Exceptions: None
+
+isContactBlocked(number<s>) -> blocked<b>::
+* number    : Phone number
+* blocked   : true=blocked, false=not blocked
+
+For unknown numbers false is returned but no exception is raised.
+
+Exceptions: InvalidPhoneNumber
+
+isRegistered() -> result<b>::
+isRegistered(number<s>) -> result<b>::
+isRegistered(numbers<as>) -> results<ab>::
+* 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<as>::
+* 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<as>) -> <>::
+* recipients : Array of phone numbers
+
+Exceptions: Failure, InvalidNumber, UntrustedIdentity
+
+sendMessage(message<s>, attachments<as>, recipient<s>) -> timestamp<x>::
+sendMessage(message<s>, attachments<as>, recipients<as>) -> timestamp<x>::
+* 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<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, recipient<s>) -> timestamp<x>::
 sendMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, recipients<as>) -> timestamp<x>::
@@ -242,18 +443,30 @@ sendMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>
 * 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<x>, groupId<ay>) -> timestamp<x>::
-* 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<s>, attachments<as>) -> timestamp<x>::
+* 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<s>, targetSentTimestamps<ax>) -> <>::
+* recipient             : Phone number of a single recipient
+* targetSentTimestamps  : Array of Longs to identify the corresponding Signal messages
+
+Exceptions: Failure, UntrustedIdentity
+
+sendViewedReceipt(recipient<s>, targetSentTimestamp<ax>) -> <>::
+* recipient             : Phone number of a single recipient
+* targetSentTimestamp   : Array of Longs to identify the corresponding signal messages
+
+Exceptions: Failure, UntrustedIdentity
 
 sendRemoteDeleteMessage(targetSentTimestamp<x>, recipient<s>) -> timestamp<x>::
 sendRemoteDeleteMessage(targetSentTimestamp<x>, recipients<as>) -> timestamp<x>::
@@ -266,111 +479,78 @@ Depending on the type of the recipient(s) field this deletes a message with one
 
 Exceptions: Failure, InvalidNumber
 
-getContactName(number<s>) -> name<s>::
-* 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<s>,name<>) -> <>::
-* number  : Phone number
-* name    : Name to be set in contacts (in local storage with signal-cli)
-
-getGroupIds() -> groupList<aay>::
-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<ay>) -> groupName<s>::
-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<ay>) -> members<as>::
-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<s>, stop<b>) -> <>::
+* recipient             : Phone number of a single recipient
+* targetSentTimestamp   : True, if typing state should be stopped
 
-listNumbers() -> numbers<as>::
-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<s>, block<b>) -> <>::
+* number  : Phone number affected by method
+* block   : false=remove block, true=blocked
 
-getContactNumber(name<s>) -> numbers<as>::
-* 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<s>) -> state<b>::
+setContactName(number<s>,name<>) -> <>::
 * number  : Phone number
-* state   : 1=blocked, 0=not blocked
-
-Exceptions: None, for unknown numbers 0 (false) is returned
-
-isGroupBlocked(groupId<ay>) -> state<b>::
-* 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<s>, expiration<i>) -> <>::
+* 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<s>) -> <>::
 * 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<s>::
-* version : Version string of signal-cli
-
-isRegistered() -> result<b>::
-isRegistered(number<s>) -> result<b>::
-isRegistered(numbers<as>) -> results<ab>::
-* 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<s>) -> <>::
-* deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app
-
-Exception: InvalidUri
-
-listDevices() -> devices<as>::
-* devices  : String array of linked devices
-
-Exception: Failure
-
-removeDevice(deviceId<i>) -> <>::
-* deviceId : Device ID to remove, obtained from listDevices() command
+Exceptions: Failure
 
-Exception: Failure
+submitRateLimitChallenge(challenge<s>, captcha<s>) -> <>::
+* 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<s>) -> <>::
-* deviceName : New name 
+Exception: IOErrorException
 
-Set a new name for this device (main or linked).
+updateProfile(name<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
+updateProfile(givenName<s>, familyName<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
+* 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<s>) -> url<s>::
 * 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<s>::
+* version : Version string of signal-cli
+
+Exceptions: None
 
 == Signals
+SyncMessageReceived (timestamp<x>, sender<s>, destination<s>, groupId<ay>, message<s>, attachments<as>)::
+* 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<x>, sender<s>, destination<s>, groupId<ay>,message<s>, attachments<as>)::
 The sync message is received when the user sends a message from a linked device.
 
 ReceiptReceived (timestamp<x>, sender<s>)::
@@ -384,7 +564,7 @@ MessageReceived(timestamp<x>, sender<s>, groupId<ay>, message<s>, attachments<as
 * 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
 
index 9829fe0006cb009dc0768310636275e36c797f44..334f740748b545ba64d5b0c9f3f22dbaa6509332 100644 (file)
@@ -144,7 +144,7 @@ Remove the registration lock pin.
 === link
 
 Link to an existing device, instead of registering a new number.
-This shows a "tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can just use this URI.
+This shows a "sgnl://linkdevice?uuid=..." URI. If you want to connect to another signal-cli instance, you can just use this URI.
 If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app.
 
 *-n* NAME, *--name* NAME::
@@ -158,7 +158,8 @@ Only works, if this is the master device.
 
 *--uri* URI::
 Specify the uri contained in the QR code shown by the new device.
-You will need the full uri enclosed in quotation marks, such as "tsdevice:/?uuid=....."
+You will need the full URI such as "sgnl://linkdevice?uuid=..." (formerly "tsdevice:/?uuid=...")
+Make sure to enclose it in quotation marks for shells.
 
 === listDevices
 
index d306dfa9daa5b1f34641a19168cfe2b036532051..b41a460deaef48a1aa5f3196cd12c2d5276934a7 100755 (executable)
@@ -6,7 +6,7 @@ fi
 
 set -e
 # To update graalvm config, set GRAALVM_HOME, e.g:
-# export GRAALVM_HOME=/usr/lib/jvm/java-11-graalvm
+# export GRAALVM_HOME=/usr/lib/jvm/java-17-graalvm
 if [ ! -z "$GRAALVM_HOME" ]; then
   export JAVA_HOME=$GRAALVM_HOME
   export SIGNAL_CLI_OPTS='-agentlib:native-image-agent=config-merge-dir=graalvm-config-dir/'
@@ -15,7 +15,7 @@ fi
 NUMBER_1="$1"
 NUMBER_2="$2"
 TEST_PIN_1=456test_pin_foo123
-NATIVE=1
+NATIVE=0
 
 PATH_TEST_CONFIG="$PWD/build/test-config"
 PATH_MAIN="$PATH_TEST_CONFIG/main"
@@ -45,7 +45,7 @@ run_linked() {
 register() {
   NUMBER=$1
   PIN=$2
-  echo -n "Enter a captcha token (https://signalcaptchas.org/registration/generate.html): "
+  echo -n "Enter a captcha token (https://signalcaptchas.org/staging/challenge/generate.html): "
   read CAPTCHA
   run_main -u "$NUMBER" register --captcha "$CAPTCHA"
   echo -n "Enter validation code for ${NUMBER}: "
@@ -81,6 +81,17 @@ register "$NUMBER_2"
 
 sleep 5
 
+
+## DBus
+#run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m daemon_not_running || true
+#run_main daemon &
+#DAEMON_PID=$!
+#sleep 10
+#run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m hii
+#run_main -u "$NUMBER_2" --dbus receive
+#kill "$DAEMON_PID"
+
+
 # JSON-RPC
 FIFO_FILE="${PATH_MAIN}/dbus-fifo"
 
@@ -184,15 +195,6 @@ done
 
 run_main -u "$NUMBER_1" removeDevice -d 2
 
-## DBus
-#run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m daemon_not_running
-#run_main daemon &
-#DAEMON_PID=$!
-#sleep 5
-#run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m hii
-#run_main -u "$NUMBER_2" --dbus receive
-#kill "$DAEMON_PID"
-
 ## Unregister
 run_main -u "$NUMBER_1" unregister
 run_main -u "$NUMBER_2" unregister --delete-account
index dad7d5d7c751defb9b92d78419a0fe025678a394..18bdbdb563a604fd0fb41a5d5fe1e79a348b5ce1 100644 (file)
@@ -1,7 +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;
@@ -84,14 +87,27 @@ public interface Signal extends DBusInterface {
 
     void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber;
 
+    @Deprecated
     void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId;
 
+    @Deprecated
     List<byte[]> getGroupIds();
 
+    DBusPath getGroup(byte[] groupId);
+
+    List<StructGroup> listGroups();
+
+    @Deprecated
     String getGroupName(byte[] groupId) throws Error.InvalidGroupId;
 
+    @Deprecated
     List<String> getGroupMembers(byte[] groupId) throws Error.InvalidGroupId;
 
+    byte[] createGroup(
+            String name, List<String> members, String avatar
+    ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
+
+    @Deprecated
     byte[] updateGroup(
             byte[] groupId, String name, List<String> members, String avatar
     ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId;
@@ -106,7 +122,7 @@ public interface Signal extends DBusInterface {
 
     DBusPath getDevice(long deviceId);
 
-    List<DBusPath> listDevices() throws Error.Failure;
+    List<StructDevice> listDevices() throws Error.Failure;
 
     DBusPath getThisDevice();
 
@@ -133,18 +149,23 @@ public interface Signal extends DBusInterface {
 
     List<String> 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 +283,37 @@ public interface Signal extends DBusInterface {
         }
     }
 
-    @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 +322,137 @@ public interface Signal extends DBusInterface {
         void removeDevice() throws Error.Failure;
     }
 
+    class StructGroup extends Struct {
+
+        @Position(0)
+        DBusPath objectPath;
+
+        @Position(1)
+        byte[] id;
+
+        @Position(2)
+        String name;
+
+        public StructGroup(final DBusPath objectPath, final byte[] id, final String name) {
+            this.objectPath = objectPath;
+            this.id = id;
+            this.name = name;
+        }
+
+        public DBusPath getObjectPath() {
+            return objectPath;
+        }
+
+        public byte[] getId() {
+            return id;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    @DBusProperty(name = "Id", type = Byte[].class, access = DBusProperty.Access.READ)
+    @DBusProperty(name = "Name", type = String.class)
+    @DBusProperty(name = "Description", type = String.class)
+    @DBusProperty(name = "Avatar", type = String.class, access = DBusProperty.Access.WRITE)
+    @DBusProperty(name = "IsBlocked", type = Boolean.class)
+    @DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ)
+    @DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ)
+    @DBusProperty(name = "MessageExpirationTimer", type = Integer.class)
+    @DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ)
+    @DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ)
+    @DBusProperty(name = "RequestingMembers", type = String[].class, access = DBusProperty.Access.READ)
+    @DBusProperty(name = "Admins", type = String[].class, access = DBusProperty.Access.READ)
+    @DBusProperty(name = "PermissionAddMember", type = String.class)
+    @DBusProperty(name = "PermissionEditDetails", type = String.class)
+    @DBusProperty(name = "PermissionSendMessage", type = String.class)
+    @DBusProperty(name = "GroupInviteLink", type = String.class, access = DBusProperty.Access.READ)
+    interface Group extends DBusInterface, Properties {
+
+        void quitGroup() throws Error.Failure, Error.LastGroupAdmin;
+
+        void addMembers(List<String> recipients) throws Error.Failure;
+
+        void removeMembers(List<String> recipients) throws Error.Failure;
+
+        void addAdmins(List<String> recipients) throws Error.Failure;
+
+        void removeAdmins(List<String> 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);
             }
         }
     }
index 911ccb615d541fe0c8200ac6c0cfb1e481ae0d4c..610ca103827b1eb5df5071a259859dff297efe39 100644 (file)
@@ -26,7 +26,7 @@ public interface SignalControl extends DBusInterface {
 
     String link(String newDeviceName) throws Error.Failure;
 
-    public String version();
+    String version();
 
     List<DBusPath> listAccounts();
 
index 73c88947c413abac3b3544d23673ef647ab61f05..96f4acb52d21738ee962c303882555da9fccf60c 100644 (file)
@@ -26,11 +26,11 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
     public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) {
         final var object = new HashMap<String, Object>();
         if (exception != null) {
-            object.put("error", new JsonError(exception));
+            object.put("error", JsonError.from(exception));
         }
 
         if (envelope != null) {
-            object.put("envelope", new JsonMessageEnvelope(envelope, content, exception, m));
+            object.put("envelope", JsonMessageEnvelope.from(envelope, content, exception, m));
         }
 
         jsonWriter.write(object);
index 35790678cd379fb3c331cf121ae48da2cfc884c2..200466ae639db971bdb510467f2dbbc70a914964 100644 (file)
@@ -6,7 +6,6 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.util.DateUtils;
-import org.asamk.signal.util.Util;
 import org.slf4j.helpers.MessageFormatter;
 import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@@ -54,8 +53,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
             writer.println("Got receipt.");
         } else if (envelope.isSignalMessage() || envelope.isPreKeySignalMessage() || envelope.isUnidentifiedSender()) {
             if (exception != null) {
-                if (exception instanceof UntrustedIdentityException) {
-                    var e = (UntrustedIdentityException) exception;
+                if (exception instanceof UntrustedIdentityException e) {
                     writer.println(
                             "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
                     final var recipientName = getLegacyIdentifier(m.resolveSignalServiceAddress(e.getSender()));
@@ -377,9 +375,6 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
             writer.println("Received sync message with verified identities:");
             final var verifiedMessage = syncMessage.getVerified().get();
             writer.println("- {}: {}", formatContact(verifiedMessage.getDestination()), verifiedMessage.getVerified());
-            var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(),
-                    verifiedMessage.getIdentityKey()));
-            writer.indentedWriter().println(safetyNumber);
         }
         if (syncMessage.getConfiguration().isPresent()) {
             writer.println("Received sync message with configuration:");
@@ -648,7 +643,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
 
         var group = m.getGroup(groupId);
         if (group != null) {
-            writer.println("Name: {}", group.getTitle());
+            writer.println("Name: {}", group.title());
         } else {
             writer.println("Name: <Unknown group>");
         }
index 516224f550cd6790eec90d9d0fd4c81c615d375e..1ec1036cd828defa7bc508c2c5aa38aedad98239 100644 (file)
@@ -52,6 +52,8 @@ public class BlockCommand implements JsonRpcLocalCommand {
             for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) {
                 try {
                     m.setGroupBlocked(groupId, true);
+                } catch (NotMasterDeviceException e) {
+                    throw new UserErrorException("This command doesn't work on linked devices.");
                 } catch (GroupNotFoundException e) {
                     logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage());
                 } catch (IOException e) {
index 02063b8721fbc7bea4de1ceb08ca0c948f3ab329..a121c7e93de4f31bc9dfb60b7fe480baf8299169 100644 (file)
@@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 
 public class DaemonCommand implements MultiLocalCommand {
 
@@ -55,6 +54,7 @@ public class DaemonCommand implements MultiLocalCommand {
             final Namespace ns, final Manager m, final OutputWriter outputWriter
     ) throws CommandException {
         boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
+        m.setIgnoreAttachments(ignoreAttachments);
 
         DBusConnection.DBusBusType busType;
         if (Boolean.TRUE.equals(ns.getBoolean("system"))) {
@@ -65,12 +65,15 @@ public class DaemonCommand implements MultiLocalCommand {
 
         try (var conn = DBusConnection.getConnection(busType)) {
             var objectPath = DbusConfig.getObjectPath();
-            var t = run(conn, objectPath, m, outputWriter, ignoreAttachments);
+            var t = run(conn, objectPath, m, outputWriter);
 
             conn.requestBusName(DbusConfig.getBusname());
 
             try {
                 t.join();
+                synchronized (this) {
+                    wait();
+                }
             } catch (InterruptedException ignored) {
             }
         } catch (DBusException | IOException e) {
@@ -94,9 +97,10 @@ public class DaemonCommand implements MultiLocalCommand {
 
         try (var conn = DBusConnection.getConnection(busType)) {
             final var signalControl = new DbusSignalControlImpl(c, m -> {
+                m.setIgnoreAttachments(ignoreAttachments);
                 try {
                     final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber());
-                    return run(conn, objectPath, m, outputWriter, ignoreAttachments);
+                    return run(conn, objectPath, m, outputWriter);
                 } catch (DBusException e) {
                     logger.error("Failed to export object", e);
                     return null;
@@ -118,7 +122,7 @@ public class DaemonCommand implements MultiLocalCommand {
     }
 
     private Thread run(
-            DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments
+            DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter
     ) throws DBusException {
         final var signal = new DbusSignalImpl(m, conn, objectPath);
         conn.exportObject(signal);
@@ -127,27 +131,11 @@ public class DaemonCommand implements MultiLocalCommand {
 
         logger.info("Exported dbus object: " + objectPath);
 
-        final var thread = new Thread(() -> {
-            while (!Thread.interrupted()) {
-                try {
-                    final var receiveMessageHandler = outputWriter instanceof JsonWriter
-                            ? new JsonDbusReceiveMessageHandler(m, (JsonWriter) outputWriter, conn, objectPath)
-                            : new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath);
-                    m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler);
-                    break;
-                } catch (IOException e) {
-                    logger.warn("Receiving messages failed, retrying", e);
-                }
-            }
-            try {
-                initThread.join();
-            } catch (InterruptedException ignored) {
-            }
-            signal.close();
-        });
-
-        thread.start();
-
-        return thread;
+        final var receiveMessageHandler = outputWriter instanceof JsonWriter ? new JsonDbusReceiveMessageHandler(m,
+                (JsonWriter) outputWriter,
+                conn,
+                objectPath) : new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath);
+        m.addReceiveHandler(receiveMessageHandler);
+        return initThread;
     }
 }
index be94fb361263fb6203d6fa1d20aac369eae0004b..b32d6633dc0cde3ead2c30a8af0d19db9c46ddfe 100644 (file)
@@ -47,8 +47,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
         }
 
         // Output
-        if (outputWriter instanceof JsonWriter) {
-            final var jsonWriter = (JsonWriter) outputWriter;
+        if (outputWriter instanceof JsonWriter jsonWriter) {
 
             var jsonUserStatuses = registered.entrySet().stream().map(entry -> {
                 final var number = entry.getValue().first();
@@ -67,21 +66,5 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
         }
     }
 
-    private static final class JsonUserStatus {
-
-        public final String recipient;
-
-        public final String number;
-
-        public final String uuid;
-
-        public final boolean isRegistered;
-
-        public JsonUserStatus(String recipient, String number, String uuid, boolean isRegistered) {
-            this.recipient = recipient;
-            this.number = number;
-            this.uuid = uuid;
-            this.isRegistered = isRegistered;
-        }
-    }
+    private record JsonUserStatus(String recipient, String number, String uuid, boolean isRegistered) {}
 }
index 1e06ea9ce9d3ca349cb76f4e0136b1e74f455876..5501c5d60cf73b368eee1483b83722a580396971 100644 (file)
@@ -55,8 +55,7 @@ public class JoinGroupCommand implements JsonRpcLocalCommand {
         try {
             final var results = m.joinGroup(linkUrl);
             var newGroupId = results.first();
-            if (outputWriter instanceof JsonWriter) {
-                final var writer = (JsonWriter) outputWriter;
+            if (outputWriter instanceof JsonWriter writer) {
                 if (!m.getGroup(newGroupId).isMember()) {
                     writer.write(Map.of("groupId", newGroupId.toBase64(), "onlyRequested", true));
                 } else {
@@ -70,7 +69,7 @@ public class JoinGroupCommand implements JsonRpcLocalCommand {
                     writer.println("Joined group \"{}\"", newGroupId.toBase64());
                 }
             }
-            handleSendMessageResults(results.second().getResults());
+            handleSendMessageResults(results.second().results());
         } catch (GroupPatchNotAcceptedException e) {
             throw new UserErrorException("Failed to join group, maybe already a member");
         } catch (IOException e) {
index 9af67322197ab1335574b66226463b2445d67941..6e0c3173d802813466d4fa491ecfe7aa03e61245 100644 (file)
@@ -33,7 +33,6 @@ import java.io.IOException;
 import java.io.InputStreamReader;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
 public class JsonRpcDispatcherCommand implements LocalCommand {
 
@@ -66,14 +65,16 @@ public class JsonRpcDispatcherCommand implements LocalCommand {
             final Namespace ns, final Manager m, final OutputWriter outputWriter
     ) throws CommandException {
         final boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
+        m.setIgnoreAttachments(ignoreAttachments);
 
         final var objectMapper = Util.createJsonObjectMapper();
         final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter);
 
-        final var receiveThread = receiveMessages(s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification(
-                "receive",
-                objectMapper.valueToTree(s),
-                null)), m, ignoreAttachments);
+        final var receiveMessageHandler = new JsonReceiveMessageHandler(m,
+                s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification("receive",
+                        objectMapper.valueToTree(s),
+                        null)));
+        m.addReceiveHandler(receiveMessageHandler);
 
         // Maybe this should be handled inside the Manager
         while (!m.hasCaughtUpWithOldMessages()) {
@@ -97,11 +98,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand {
         jsonRpcReader.readRequests((method, params) -> handleRequest(m, objectMapper, method, params),
                 response -> logger.debug("Received unexpected response for id {}", response.getId()));
 
-        receiveThread.interrupt();
-        try {
-            receiveThread.join();
-        } catch (InterruptedException ignored) {
-        }
+        m.removeReceiveHandler(receiveMessageHandler);
     }
 
     private JsonNode handleRequest(
@@ -166,24 +163,4 @@ public class JsonRpcDispatcherCommand implements LocalCommand {
         }
         command.handleCommand(requestParams, m, outputWriter);
     }
-
-    private Thread receiveMessages(
-            JsonWriter jsonWriter, Manager m, boolean ignoreAttachments
-    ) {
-        final var thread = new Thread(() -> {
-            while (!Thread.interrupted()) {
-                try {
-                    final var receiveMessageHandler = new JsonReceiveMessageHandler(m, jsonWriter);
-                    m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler);
-                    break;
-                } catch (IOException e) {
-                    logger.warn("Receiving messages failed, retrying", e);
-                }
-            }
-        });
-
-        thread.start();
-
-        return thread;
-    }
 }
index 5b926732710b183bb32205a617b97684d835edbb..ccf327715d67966ce89f55115b1184b7b29f918e 100644 (file)
@@ -16,8 +16,7 @@ import java.util.Map;
 public interface JsonRpcLocalCommand extends JsonRpcCommand<Map<String, Object>>, LocalCommand {
 
     default TypeReference<Map<String, Object>> getRequestType() {
-        return new TypeReference<>() {
-        };
+        return new TypeReference<>() {};
     }
 
     default void handleCommand(
@@ -32,7 +31,7 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand<Map<String, Object>>
     }
 
     /**
-     * Namepace implementation, that defaults booleans to false and converts camel case keys to dashed strings
+     * Namespace implementation, that has plural handling for list arguments and converts camel case keys to dashed strings
      */
     final class JsonRpcNamespace extends Namespace {
 
index b6dfc3ce229c92d2320ae579754dbb0035e6b42d..b87cba7edf42c805956db504c70a1878a332c279 100644 (file)
@@ -27,8 +27,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
     public void handleCommand(final Namespace ns, final Manager m, final OutputWriter outputWriter) {
         var contacts = m.getContacts();
 
-        if (outputWriter instanceof PlainTextWriter) {
-            final var writer = (PlainTextWriter) outputWriter;
+        if (outputWriter instanceof PlainTextWriter writer) {
             for (var c : contacts) {
                 final var contact = c.second();
                 writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}",
@@ -55,26 +54,5 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
         }
     }
 
-    private static final class JsonContact {
-
-        public final String number;
-        public final String uuid;
-        public final String name;
-        public final boolean isBlocked;
-        public final int messageExpirationTime;
-
-        private JsonContact(
-                final String number,
-                final String uuid,
-                final String name,
-                final boolean isBlocked,
-                final int messageExpirationTime
-        ) {
-            this.number = number;
-            this.uuid = uuid;
-            this.name = name;
-            this.isBlocked = isBlocked;
-            this.messageExpirationTime = messageExpirationTime;
-        }
-    }
+    private record JsonContact(String number, String uuid, String name, boolean isBlocked, int messageExpirationTime) {}
 }
index 1de5b84222fa610e730b905f21e5b73da4f2af94..6db929b0d9870d20a2e119dc07f51e2d906bcbea 100644 (file)
@@ -43,39 +43,23 @@ public class ListDevicesCommand implements JsonRpcLocalCommand {
             throw new IOErrorException("Failed to get linked devices: " + e.getMessage(), e);
         }
 
-        if (outputWriter instanceof PlainTextWriter) {
-            final var writer = (PlainTextWriter) outputWriter;
+        if (outputWriter instanceof PlainTextWriter writer) {
             for (var d : devices) {
-                writer.println("- Device {}{}:", d.getId(), (d.isThisDevice() ? " (this device)" : ""));
+                writer.println("- Device {}{}:", d.id(), (d.isThisDevice() ? " (this device)" : ""));
                 writer.indent(w -> {
-                    w.println("Name: {}", d.getName());
-                    w.println("Created: {}", DateUtils.formatTimestamp(d.getCreated()));
-                    w.println("Last seen: {}", DateUtils.formatTimestamp(d.getLastSeen()));
+                    w.println("Name: {}", d.name());
+                    w.println("Created: {}", DateUtils.formatTimestamp(d.created()));
+                    w.println("Last seen: {}", DateUtils.formatTimestamp(d.lastSeen()));
                 });
             }
         } else {
             final var writer = (JsonWriter) outputWriter;
             final var jsonDevices = devices.stream()
-                    .map(d -> new JsonDevice(d.getId(), d.getName(), d.getCreated(), d.getLastSeen()))
+                    .map(d -> new JsonDevice(d.id(), d.name(), d.created(), d.lastSeen()))
                     .collect(Collectors.toList());
             writer.write(jsonDevices);
         }
     }
 
-    private static final class JsonDevice {
-
-        public final long id;
-        public final String name;
-        public final long createdTimestamp;
-        public final long lastSeenTimestamp;
-
-        private JsonDevice(
-                final long id, final String name, final long createdTimestamp, final long lastSeenTimestamp
-        ) {
-            this.id = id;
-            this.name = name;
-            this.createdTimestamp = createdTimestamp;
-            this.lastSeenTimestamp = lastSeenTimestamp;
-        }
-    }
+    private record JsonDevice(long id, String name, long createdTimestamp, long lastSeenTimestamp) {}
 }
index fd8c4b92141bdeb838276c25aa16a31fc82e63dd..ef5fd452e6c41a95b47c336e83e9792ac26a4dde 100644 (file)
@@ -50,25 +50,25 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
             PlainTextWriter writer, Group group, boolean detailed
     ) {
         if (detailed) {
-            final var groupInviteLink = group.getGroupInviteLinkUrl();
+            final var groupInviteLink = group.groupInviteLinkUrl();
 
             writer.println(
                     "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Message expiration: {} Link: {}",
-                    group.getGroupId().toBase64(),
-                    group.getTitle(),
-                    group.getDescription(),
+                    group.groupId().toBase64(),
+                    group.title(),
+                    group.description(),
                     group.isMember(),
                     group.isBlocked(),
-                    resolveMembers(group.getMembers()),
-                    resolveMembers(group.getPendingMembers()),
-                    resolveMembers(group.getRequestingMembers()),
-                    resolveMembers(group.getAdminMembers()),
-                    group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s",
+                    resolveMembers(group.members()),
+                    resolveMembers(group.pendingMembers()),
+                    resolveMembers(group.requestingMembers()),
+                    resolveMembers(group.adminMembers()),
+                    group.messageExpirationTimer() == 0 ? "disabled" : group.messageExpirationTimer() + "s",
                     groupInviteLink == null ? '-' : groupInviteLink.getUrl());
         } else {
             writer.println("Id: {} Name: {}  Active: {} Blocked: {}",
-                    group.getGroupId().toBase64(),
-                    group.getTitle(),
+                    group.groupId().toBase64(),
+                    group.title(),
                     group.isMember(),
                     group.isBlocked());
         }
@@ -80,22 +80,24 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
     ) throws CommandException {
         final var groups = m.getGroups();
 
-        if (outputWriter instanceof JsonWriter) {
-            final var jsonWriter = (JsonWriter) outputWriter;
+        if (outputWriter instanceof JsonWriter jsonWriter) {
 
             var jsonGroups = groups.stream().map(group -> {
-                final var groupInviteLink = group.getGroupInviteLinkUrl();
+                final var groupInviteLink = group.groupInviteLinkUrl();
 
-                return new JsonGroup(group.getGroupId().toBase64(),
-                        group.getTitle(),
-                        group.getDescription(),
+                return new JsonGroup(group.groupId().toBase64(),
+                        group.title(),
+                        group.description(),
                         group.isMember(),
                         group.isBlocked(),
-                        group.getMessageExpirationTime(),
-                        resolveJsonMembers(group.getMembers()),
-                        resolveJsonMembers(group.getPendingMembers()),
-                        resolveJsonMembers(group.getRequestingMembers()),
-                        resolveJsonMembers(group.getAdminMembers()),
+                        group.messageExpirationTimer(),
+                        resolveJsonMembers(group.members()),
+                        resolveJsonMembers(group.pendingMembers()),
+                        resolveJsonMembers(group.requestingMembers()),
+                        resolveJsonMembers(group.adminMembers()),
+                        group.permissionAddMember().name(),
+                        group.permissionEditDetails().name(),
+                        group.permissionSendMessage().name(),
                         groupInviteLink == null ? null : groupInviteLink.getUrl());
             }).collect(Collectors.toList());
 
@@ -109,57 +111,22 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
         }
     }
 
-    private static final class JsonGroup {
-
-        public final String id;
-        public final String name;
-        public final String description;
-        public final boolean isMember;
-        public final boolean isBlocked;
-        public final int messageExpirationTime;
-
-        public final Set<JsonGroupMember> members;
-        public final Set<JsonGroupMember> pendingMembers;
-        public final Set<JsonGroupMember> requestingMembers;
-        public final Set<JsonGroupMember> admins;
-        public final String groupInviteLink;
-
-        public JsonGroup(
-                String id,
-                String name,
-                String description,
-                boolean isMember,
-                boolean isBlocked,
-                final int messageExpirationTime,
-                Set<JsonGroupMember> members,
-                Set<JsonGroupMember> pendingMembers,
-                Set<JsonGroupMember> requestingMembers,
-                Set<JsonGroupMember> admins,
-                String groupInviteLink
-        ) {
-            this.id = id;
-            this.name = name;
-            this.description = description;
-            this.isMember = isMember;
-            this.isBlocked = isBlocked;
-            this.messageExpirationTime = messageExpirationTime;
-
-            this.members = members;
-            this.pendingMembers = pendingMembers;
-            this.requestingMembers = requestingMembers;
-            this.admins = admins;
-            this.groupInviteLink = groupInviteLink;
-        }
-    }
-
-    private static final class JsonGroupMember {
-
-        public final String number;
-        public final String uuid;
-
-        private JsonGroupMember(final String number, final String uuid) {
-            this.number = number;
-            this.uuid = uuid;
-        }
-    }
+    private record JsonGroup(
+            String id,
+            String name,
+            String description,
+            boolean isMember,
+            boolean isBlocked,
+            int messageExpirationTime,
+            Set<JsonGroupMember> members,
+            Set<JsonGroupMember> pendingMembers,
+            Set<JsonGroupMember> requestingMembers,
+            Set<JsonGroupMember> admins,
+            String permissionAddMember,
+            String permissionEditDetails,
+            String permissionSendMessage,
+            String groupInviteLink
+    ) {}
+
+    private record JsonGroupMember(String number, String uuid) {}
 }
index ed2942a5f7d836656db3995e7906631f6dd886a7..6f6fac2f49ce11ce1aad67690c304ce318fe7900 100644 (file)
@@ -30,12 +30,12 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
     }
 
     private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, Identity theirId) {
-        final SignalServiceAddress address = theirId.getRecipient().toSignalServiceAddress();
-        var digits = Util.formatSafetyNumber(theirId.getSafetyNumber());
+        final SignalServiceAddress address = theirId.recipient().toSignalServiceAddress();
+        var digits = Util.formatSafetyNumber(theirId.safetyNumber());
         writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}",
                 address.getNumber().orNull(),
-                theirId.getTrustLevel(),
-                theirId.getDateAdded(),
+                theirId.trustLevel(),
+                theirId.dateAdded(),
                 Hex.toString(theirId.getFingerprint()),
                 digits);
     }
@@ -59,17 +59,16 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
             identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getSelfNumber()));
         }
 
-        if (outputWriter instanceof PlainTextWriter) {
-            final var writer = (PlainTextWriter) outputWriter;
+        if (outputWriter instanceof PlainTextWriter writer) {
             for (var id : identities) {
                 printIdentityFingerprint(writer, m, id);
             }
         } else {
             final var writer = (JsonWriter) outputWriter;
             final var jsonIdentities = identities.stream().map(id -> {
-                final var address = id.getRecipient().toSignalServiceAddress();
-                var safetyNumber = Util.formatSafetyNumber(id.getSafetyNumber());
-                var scannableSafetyNumber = id.getScannableSafetyNumber();
+                final var address = id.recipient().toSignalServiceAddress();
+                var safetyNumber = Util.formatSafetyNumber(id.safetyNumber());
+                var scannableSafetyNumber = id.scannableSafetyNumber();
                 return new JsonIdentity(address.getNumber().orNull(),
                         address.getUuid().toString(),
                         Hex.toString(id.getFingerprint()),
@@ -77,40 +76,21 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
                         scannableSafetyNumber == null
                                 ? null
                                 : Base64.getEncoder().encodeToString(scannableSafetyNumber),
-                        id.getTrustLevel().name(),
-                        id.getDateAdded().getTime());
+                        id.trustLevel().name(),
+                        id.dateAdded().getTime());
             }).collect(Collectors.toList());
 
             writer.write(jsonIdentities);
         }
     }
 
-    private static final class JsonIdentity {
-
-        public final String number;
-        public final String uuid;
-        public final String fingerprint;
-        public final String safetyNumber;
-        public final String scannableSafetyNumber;
-        public final String trustLevel;
-        public final long addedTimestamp;
-
-        private JsonIdentity(
-                final String number,
-                final String uuid,
-                final String fingerprint,
-                final String safetyNumber,
-                final String scannableSafetyNumber,
-                final String trustLevel,
-                final long addedTimestamp
-        ) {
-            this.number = number;
-            this.uuid = uuid;
-            this.fingerprint = fingerprint;
-            this.safetyNumber = safetyNumber;
-            this.scannableSafetyNumber = scannableSafetyNumber;
-            this.trustLevel = trustLevel;
-            this.addedTimestamp = addedTimestamp;
-        }
-    }
+    private record JsonIdentity(
+            String number,
+            String uuid,
+            String fingerprint,
+            String safetyNumber,
+            String scannableSafetyNumber,
+            String trustLevel,
+            long addedTimestamp
+    ) {}
 }
index 1d6611b58b814214be22b8828d6a0cdf2ba1eda5..25647e4eb27bbd5b9af5eb14b16899f398be1fc4 100644 (file)
@@ -55,9 +55,9 @@ public class QuitGroupCommand implements JsonRpcLocalCommand {
         try {
             try {
                 final var results = m.quitGroup(groupId, groupAdmins);
-                final var timestamp = results.getTimestamp();
+                final var timestamp = results.timestamp();
                 outputResult(outputWriter, timestamp);
-                handleSendMessageResults(results.getResults());
+                handleSendMessageResults(results.results());
             } catch (NotAGroupMemberException e) {
                 logger.info("User is not a group member");
             }
@@ -79,8 +79,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand {
     }
 
     private void outputResult(final OutputWriter outputWriter, final long timestamp) {
-        if (outputWriter instanceof PlainTextWriter) {
-            final var writer = (PlainTextWriter) outputWriter;
+        if (outputWriter instanceof PlainTextWriter writer) {
             writer.println("{}", timestamp);
         } else {
             final var writer = (JsonWriter) outputWriter;
index 4686f26dbcabecc409898ecf56d29fb060badaa9..fe062ffc58b116bae801d6976f8b482e6537dd40 100644 (file)
@@ -58,23 +58,22 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
             final Namespace ns, final Signal signal, DBusConnection dbusconnection, final OutputWriter outputWriter
     ) throws CommandException {
         try {
-            if (outputWriter instanceof JsonWriter) {
-                final var jsonWriter = (JsonWriter) outputWriter;
+            if (outputWriter instanceof JsonWriter jsonWriter) {
 
                 dbusconnection.addSigHandler(Signal.MessageReceived.class, signal, messageReceived -> {
-                    var envelope = new JsonMessageEnvelope(messageReceived);
+                    var envelope = JsonMessageEnvelope.from(messageReceived);
                     final var object = Map.of("envelope", envelope);
                     jsonWriter.write(object);
                 });
 
                 dbusconnection.addSigHandler(Signal.ReceiptReceived.class, signal, receiptReceived -> {
-                    var envelope = new JsonMessageEnvelope(receiptReceived);
+                    var envelope = JsonMessageEnvelope.from(receiptReceived);
                     final var object = Map.of("envelope", envelope);
                     jsonWriter.write(object);
                 });
 
                 dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, signal, syncReceived -> {
-                    var envelope = new JsonMessageEnvelope(syncReceived);
+                    var envelope = JsonMessageEnvelope.from(syncReceived);
                     final var object = Map.of("envelope", envelope);
                     jsonWriter.write(object);
                 });
@@ -128,11 +127,18 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
             logger.error("Dbus client failed", e);
             throw new UnexpectedErrorException("Dbus client failed", e);
         }
+
+        double timeout = ns.getDouble("timeout");
+        long timeoutMilliseconds = timeout < 0 ? 10000 : (long) (timeout * 1000);
+
         while (true) {
             try {
-                Thread.sleep(10000);
+                Thread.sleep(timeoutMilliseconds);
             } catch (InterruptedException ignored) {
-                return;
+                break;
+            }
+            if (timeout >= 0) {
+                break;
             }
         }
     }
@@ -142,20 +148,16 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
             final Namespace ns, final Manager m, final OutputWriter outputWriter
     ) throws CommandException {
         double timeout = ns.getDouble("timeout");
-        var returnOnTimeout = true;
-        if (timeout < 0) {
-            returnOnTimeout = false;
-            timeout = 3600;
-        }
         boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
+        m.setIgnoreAttachments(ignoreAttachments);
         try {
             final var handler = outputWriter instanceof JsonWriter ? new JsonReceiveMessageHandler(m,
                     (JsonWriter) outputWriter) : new ReceiveMessageHandler(m, (PlainTextWriter) outputWriter);
-            m.receiveMessages((long) (timeout * 1000),
-                    TimeUnit.MILLISECONDS,
-                    returnOnTimeout,
-                    ignoreAttachments,
-                    handler);
+            if (timeout < 0) {
+                m.receiveMessages(handler);
+            } else {
+                m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, handler);
+            }
         } catch (IOException e) {
             throw new IOErrorException("Error while receiving messages: " + e.getMessage(), e);
         }
index af6c06ad3916ce6e756f6ebdbc131e66a5d1dc8c..7e4fe505978ef78017070b7ca866687c5e32e662 100644 (file)
@@ -40,10 +40,11 @@ public class RegisterCommand implements RegistrationCommand {
         } catch (CaptchaRequiredException e) {
             String message;
             if (captcha == null) {
-                message = "Captcha required for verification, use --captcha CAPTCHA\n"
-                        + "To get the token, go to https://signalcaptchas.org/registration/generate.html\n"
-                        + "Check the developer tools (F12) console for a failed redirect to signalcaptcha://\n"
-                        + "Everything after signalcaptcha:// is the captcha token.";
+                message = """
+                        Captcha required for verification, use --captcha CAPTCHA
+                        To get the token, go to https://signalcaptchas.org/registration/generate.html
+                        Check the developer tools (F12) console for a failed redirect to signalcaptcha://
+                        Everything after signalcaptcha:// is the captcha token.""";
             } else {
                 message = "Invalid captcha given.";
             }
index c9eab95ca57d5ab06f01b9e5fd7c7aace92135f2..2b9ebe918e4e748be3a0b6acbd78d4b94eb2ca1c 100644 (file)
@@ -56,8 +56,8 @@ public class RemoteDeleteCommand implements JsonRpcLocalCommand {
 
         try {
             final var results = m.sendRemoteDeleteMessage(targetTimestamp, recipientIdentifiers);
-            outputResult(outputWriter, results.getTimestamp());
-            ErrorUtils.handleSendMessageResults(results.getResults());
+            outputResult(outputWriter, results.timestamp());
+            ErrorUtils.handleSendMessageResults(results.results());
         } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
             throw new UserErrorException(e.getMessage());
         } catch (IOException e) {
@@ -67,8 +67,7 @@ public class RemoteDeleteCommand implements JsonRpcLocalCommand {
     }
 
     private void outputResult(final OutputWriter outputWriter, final long timestamp) {
-        if (outputWriter instanceof PlainTextWriter) {
-            final var writer = (PlainTextWriter) outputWriter;
+        if (outputWriter instanceof PlainTextWriter writer) {
             writer.println("{}", timestamp);
         } else {
             final var writer = (JsonWriter) outputWriter;
index dba7689f2fbad2a2e8b5600db8399d41d35bc025..e22d307b7f3c4ad239d77fdfa3bd4b76c7ee1bde 100644 (file)
@@ -102,8 +102,8 @@ public class SendCommand implements JsonRpcLocalCommand {
 
         try {
             var results = m.sendMessage(new Message(messageText, attachments), recipientIdentifiers);
-            outputResult(outputWriter, results.getTimestamp());
-            ErrorUtils.handleSendMessageResults(results.getResults());
+            outputResult(outputWriter, results.timestamp());
+            ErrorUtils.handleSendMessageResults(results.results());
         } catch (AttachmentInvalidException | IOException e) {
             throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
                     .getSimpleName() + ")", e);
@@ -113,8 +113,7 @@ public class SendCommand implements JsonRpcLocalCommand {
     }
 
     private void outputResult(final OutputWriter outputWriter, final long timestamp) {
-        if (outputWriter instanceof PlainTextWriter) {
-            final var writer = (PlainTextWriter) outputWriter;
+        if (outputWriter instanceof PlainTextWriter writer) {
             writer.println("{}", timestamp);
         } else {
             final var writer = (JsonWriter) outputWriter;
index 857f603dc2f3f7bea794ca3e409a820550201adb..3992dc559893bd915bb6fe653047da775febfcc3 100644 (file)
@@ -72,8 +72,8 @@ public class SendReactionCommand implements JsonRpcLocalCommand {
                     CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
                     targetTimestamp,
                     recipientIdentifiers);
-            outputResult(outputWriter, results.getTimestamp());
-            ErrorUtils.handleSendMessageResults(results.getResults());
+            outputResult(outputWriter, results.timestamp());
+            ErrorUtils.handleSendMessageResults(results.results());
         } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
             throw new UserErrorException(e.getMessage());
         } catch (IOException e) {
@@ -83,8 +83,7 @@ public class SendReactionCommand implements JsonRpcLocalCommand {
     }
 
     private void outputResult(final OutputWriter outputWriter, final long timestamp) {
-        if (outputWriter instanceof PlainTextWriter) {
-            final var writer = (PlainTextWriter) outputWriter;
+        if (outputWriter instanceof PlainTextWriter writer) {
             writer.println("{}", timestamp);
         } else {
             final var writer = (JsonWriter) outputWriter;
index 7cf209fa05fe39adf9d6c0824560c732b409f286..53eab3b3b728d5b69bfaee45c8022bb7f213504c 100644 (file)
@@ -51,6 +51,8 @@ public class UnblockCommand implements JsonRpcLocalCommand {
         for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) {
             try {
                 m.setGroupBlocked(groupId, false);
+            } catch (NotMasterDeviceException e) {
+                throw new UserErrorException("This command doesn't work on linked devices.");
             } catch (GroupNotFoundException e) {
                 logger.warn("Unknown group id: {}", groupId);
             } catch (IOException e) {
index 68bce2d222a0a3756e3b4635aad57517cc5554c9..20e70278e55f498025e31a818d72d937ca859af1 100644 (file)
@@ -12,6 +12,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
 import org.asamk.signal.commands.exceptions.UserErrorException;
 import org.asamk.signal.manager.AttachmentInvalidException;
 import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupLinkState;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
@@ -76,33 +77,23 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
         if (value == null) {
             return null;
         }
-        switch (value) {
-            case "enabled":
-                return GroupLinkState.ENABLED;
-            case "enabled-with-approval":
-            case "enabledWithApproval":
-                return GroupLinkState.ENABLED_WITH_APPROVAL;
-            case "disabled":
-                return GroupLinkState.DISABLED;
-            default:
-                throw new UserErrorException("Invalid group link state: " + value);
-        }
+        return switch (value) {
+            case "enabled" -> GroupLinkState.ENABLED;
+            case "enabled-with-approval", "enabledWithApproval" -> GroupLinkState.ENABLED_WITH_APPROVAL;
+            case "disabled" -> GroupLinkState.DISABLED;
+            default -> throw new UserErrorException("Invalid group link state: " + value);
+        };
     }
 
     GroupPermission getGroupPermission(String value) throws UserErrorException {
         if (value == null) {
             return null;
         }
-        switch (value) {
-            case "every-member":
-            case "everyMember":
-                return GroupPermission.EVERY_MEMBER;
-            case "only-admins":
-            case "onlyAdmins":
-                return GroupPermission.ONLY_ADMINS;
-            default:
-                throw new UserErrorException("Invalid group permission: " + value);
-        }
+        return switch (value) {
+            case "every-member", "everyMember" -> GroupPermission.EVERY_MEMBER;
+            case "only-admins", "onlyAdmins" -> GroupPermission.ONLY_ADMINS;
+            default -> throw new UserErrorException("Invalid group permission: " + value);
+        };
     }
 
     @Override
@@ -136,8 +127,8 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
                 var results = m.createGroup(groupName,
                         groupMembers,
                         groupAvatar == null ? null : new File(groupAvatar));
-                timestamp = results.second().getTimestamp();
-                ErrorUtils.handleSendMessageResults(results.second().getResults());
+                timestamp = results.second().timestamp();
+                ErrorUtils.handleSendMessageResults(results.second().results());
                 groupId = results.first();
                 groupName = null;
                 groupMembers = null;
@@ -145,24 +136,26 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
             }
 
             var results = m.updateGroup(groupId,
-                    groupName,
-                    groupDescription,
-                    groupMembers,
-                    groupRemoveMembers,
-                    groupAdmins,
-                    groupRemoveAdmins,
-                    groupResetLink,
-                    groupLinkState,
-                    groupAddMemberPermission,
-                    groupEditDetailsPermission,
-                    groupAvatar == null ? null : new File(groupAvatar),
-                    groupExpiration,
-                    groupSendMessagesPermission == null
-                            ? null
-                            : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS);
+                    UpdateGroup.newBuilder()
+                            .withName(groupName)
+                            .withDescription(groupDescription)
+                            .withMembers(groupMembers)
+                            .withRemoveMembers(groupRemoveMembers)
+                            .withAdmins(groupAdmins)
+                            .withRemoveAdmins(groupRemoveAdmins)
+                            .withResetGroupLink(groupResetLink)
+                            .withGroupLinkState(groupLinkState)
+                            .withAddMemberPermission(groupAddMemberPermission)
+                            .withEditDetailsPermission(groupEditDetailsPermission)
+                            .withAvatarFile(groupAvatar == null ? null : new File(groupAvatar))
+                            .withExpirationTimer(groupExpiration)
+                            .withIsAnnouncementGroup(groupSendMessagesPermission == null
+                                    ? null
+                                    : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS)
+                            .build());
             if (results != null) {
-                timestamp = results.getTimestamp();
-                ErrorUtils.handleSendMessageResults(results.getResults());
+                timestamp = results.timestamp();
+                ErrorUtils.handleSendMessageResults(results.results());
             }
             outputResult(outputWriter, timestamp, isNewGroup ? groupId : null);
         } catch (AttachmentInvalidException e) {
@@ -176,8 +169,7 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
     }
 
     private void outputResult(final OutputWriter outputWriter, final Long timestamp, final GroupId groupId) {
-        if (outputWriter instanceof PlainTextWriter) {
-            final var writer = (PlainTextWriter) outputWriter;
+        if (outputWriter instanceof PlainTextWriter writer) {
             if (groupId != null) {
                 writer.println("Created new group: \"{}\"", groupId.toBase64());
             }
index 53b64b8c64dfd477da46e752ff315fd568edecde..23d293655e7eb37515d6c773b728ca8e71c106b3 100644 (file)
@@ -42,8 +42,7 @@ public class UploadStickerPackCommand implements JsonRpcLocalCommand {
 
         try {
             var url = m.uploadStickerPack(path);
-            if (outputWriter instanceof PlainTextWriter) {
-                final var writer = (PlainTextWriter) outputWriter;
+            if (outputWriter instanceof PlainTextWriter writer) {
                 writer.println("{}", url);
             } else {
                 final var writer = (JsonWriter) outputWriter;
index 3124a5b05ed8ee655418a466bab56c44c5edc83d..9ae58122fc752e73ae6877ad0089dcfee8b4456b 100644 (file)
@@ -15,9 +15,9 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.api.SendGroupMessageResults;
 import org.asamk.signal.manager.api.SendMessageResults;
 import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
-import org.asamk.signal.manager.groups.GroupLinkState;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
 import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
@@ -30,7 +30,6 @@ import org.freedesktop.dbus.DBusPath;
 import org.freedesktop.dbus.connections.impl.DBusConnection;
 import org.freedesktop.dbus.exceptions.DBusException;
 import org.freedesktop.dbus.interfaces.DBusInterface;
-import org.whispersystems.libsignal.IdentityKey;
 import org.whispersystems.libsignal.InvalidKeyException;
 import org.whispersystems.libsignal.util.Pair;
 import org.whispersystems.libsignal.util.guava.Optional;
@@ -145,13 +144,14 @@ public class DbusManagerImpl implements Manager {
     @Override
     public List<Device> getLinkedDevices() throws IOException {
         final var thisDevice = signal.getThisDevice();
-        return signal.listDevices().stream().map(devicePath -> {
-            final var device = getRemoteObject(devicePath, Signal.Device.class).GetAll("org.asamk.Signal.Device");
+        return signal.listDevices().stream().map(d -> {
+            final var device = getRemoteObject(d.getObjectPath(),
+                    Signal.Device.class).GetAll("org.asamk.Signal.Device");
             return new Device((long) device.get("Id").getValue(),
                     (String) device.get("Name").getValue(),
                     (long) device.get("Created").getValue(),
                     (long) device.get("LastSeen").getValue(),
-                    thisDevice.equals(devicePath));
+                    thisDevice.equals(d.getObjectPath()));
         }).collect(Collectors.toList());
     }
 
@@ -182,8 +182,8 @@ public class DbusManagerImpl implements Manager {
 
     @Override
     public List<Group> 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
@@ -193,7 +193,8 @@ public class DbusManagerImpl implements Manager {
         if (groupAdmins.size() > 0) {
             throw new UnsupportedOperationException();
         }
-        signal.quitGroup(groupId.serialize());
+        final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
+        group.quitGroup();
         return new SendGroupMessageResults(0, List.of());
     }
 
@@ -206,8 +207,7 @@ public class DbusManagerImpl implements Manager {
     public Pair<GroupId, SendGroupMessageResults> createGroup(
             final String name, final Set<RecipientIdentifier.Single> 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()));
@@ -215,25 +215,70 @@ public class DbusManagerImpl implements Manager {
 
     @Override
     public SendGroupMessageResults updateGroup(
-            final GroupId groupId,
-            final String name,
-            final String description,
-            final Set<RecipientIdentifier.Single> members,
-            final Set<RecipientIdentifier.Single> removeMembers,
-            final Set<RecipientIdentifier.Single> admins,
-            final Set<RecipientIdentifier.Single> 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();
+                case ENABLED -> group.enableLink(false);
+                case ENABLED_WITH_APPROVAL -> group.enableLink(true);
+            }
+        }
         return new SendGroupMessageResults(0, List.of());
     }
 
@@ -276,9 +321,9 @@ public class DbusManagerImpl implements Manager {
             final Message message, final Set<RecipientIdentifier> recipients
     ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
         return handleMessage(recipients,
-                numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers),
-                () -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()),
-                groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId));
+                numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers),
+                () -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()),
+                groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
     }
 
     @Override
@@ -343,7 +388,12 @@ public class DbusManagerImpl implements Manager {
     public void setGroupBlocked(
             final GroupId groupId, final boolean blocked
     ) throws GroupNotFoundException, IOException {
-        signal.setGroupBlocked(groupId.serialize(), blocked);
+        setGroupProperty(groupId, "IsBlocked", blocked);
+    }
+
+    private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
+        final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
+        group.Set("org.asamk.Signal.Group", propertyName, blocked);
     }
 
     @Override
@@ -367,17 +417,38 @@ public class DbusManagerImpl implements Manager {
         signal.sendSyncRequest();
     }
 
+    @Override
+    public void addReceiveHandler(final ReceiveMessageHandler handler) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeReceiveHandler(final ReceiveMessageHandler handler) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isReceiving() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void receiveMessages(final ReceiveMessageHandler handler) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
     @Override
     public void receiveMessages(
-            final long timeout,
-            final TimeUnit unit,
-            final boolean returnOnTimeout,
-            final boolean ignoreAttachments,
-            final ReceiveMessageHandler handler
+            final long timeout, final TimeUnit unit, final ReceiveMessageHandler handler
     ) throws IOException {
         throw new UnsupportedOperationException();
     }
 
+    @Override
+    public void setIgnoreAttachments(final boolean ignoreAttachments) {
+        throw new UnsupportedOperationException();
+    }
+
     @Override
     public boolean hasCaughtUpWithOldMessages() {
         throw new UnsupportedOperationException();
@@ -410,19 +481,41 @@ public class DbusManagerImpl implements Manager {
 
     @Override
     public Group getGroup(final GroupId groupId) {
-        final var id = groupId.serialize();
-        return new Group(groupId,
-                signal.getGroupName(id),
-                null,
-                null,
-                signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()),
-                Set.of(),
-                Set.of(),
-                Set.of(),
-                signal.isGroupBlocked(id),
-                0,
-                false,
-                signal.isMember(id));
+        final var groupPath = signal.getGroup(groupId.serialize());
+        return getGroup(groupPath);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Group getGroup(final DBusPath groupPath) {
+        final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
+        final var id = (byte[]) group.get("Id").getValue();
+        try {
+            return new Group(GroupId.unknownVersion(id),
+                    (String) group.get("Name").getValue(),
+                    (String) group.get("Description").getValue(),
+                    GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
+                    ((List<String>) group.get("Members").getValue()).stream()
+                            .map(m -> new RecipientAddress(null, m))
+                            .collect(Collectors.toSet()),
+                    ((List<String>) group.get("PendingMembers").getValue()).stream()
+                            .map(m -> new RecipientAddress(null, m))
+                            .collect(Collectors.toSet()),
+                    ((List<String>) group.get("RequestingMembers").getValue()).stream()
+                            .map(m -> new RecipientAddress(null, m))
+                            .collect(Collectors.toSet()),
+                    ((List<String>) 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
@@ -459,13 +552,6 @@ public class DbusManagerImpl implements Manager {
         throw new UnsupportedOperationException();
     }
 
-    @Override
-    public String computeSafetyNumber(
-            final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
-    ) {
-        throw new UnsupportedOperationException();
-    }
-
     @Override
     public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
         return address;
index 37cc35e340099cf2174bba29648d191e3b6ce2b3..bbe01d6b581dc9e85025c2b222b64d51f7aa666a 100644 (file)
@@ -51,6 +51,7 @@ public abstract class DbusProperties implements Properties {
     }
 
     @Override
+    @SuppressWarnings("unchecked")
     public Map<String, Variant<?>> GetAll(final String interface_name) {
         final var handler = getHandlerOptional(interface_name);
         if (handler.isEmpty()) {
@@ -61,6 +62,9 @@ public abstract class DbusProperties implements Properties {
                 .getProperties()
                 .stream()
                 .filter(p -> p.getGetter() != null)
-                .collect(Collectors.toMap(DbusProperty::getName, p -> new Variant<>(p.getGetter().get())));
+                .collect(Collectors.toMap(DbusProperty::getName, p -> {
+                    final Object o = p.getGetter().get();
+                    return o instanceof Variant ? (Variant<Object>) o : new Variant<>(o);
+                }));
     }
 }
index e055778699b63ba7bf98c85e57733b7a230b4276..5042458ebd269198e7f401693dbffb30a08b30e8 100644 (file)
@@ -21,6 +21,12 @@ public class DbusProperty<T> {
         this.setter = null;
     }
 
+    public DbusProperty(final String name, final Consumer<T> setter) {
+        this.name = name;
+        this.getter = null;
+        this.setter = setter;
+    }
+
     public String getName() {
         return name;
     }
index 18565a0d1daaa38b4d650dd91ca02106cbb84ede..977dfa758cc2ec98c8a3bcf3c72c84d36a942adc 100644 (file)
@@ -2,6 +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 @@ import org.asamk.signal.manager.api.Identity;
 import org.asamk.signal.manager.api.Message;
 import org.asamk.signal.manager.api.RecipientIdentifier;
 import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UpdateGroup;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupLinkState;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
 import org.asamk.signal.manager.groups.LastGroupAdminException;
 import org.asamk.signal.manager.groups.NotAGroupMemberException;
@@ -24,6 +28,7 @@ import org.freedesktop.dbus.DBusPath;
 import org.freedesktop.dbus.connections.impl.DBusConnection;
 import org.freedesktop.dbus.exceptions.DBusException;
 import org.freedesktop.dbus.exceptions.DBusExecutionException;
+import org.freedesktop.dbus.types.Variant;
 import org.whispersystems.libsignal.InvalidKeyException;
 import org.whispersystems.libsignal.util.Pair;
 import org.whispersystems.libsignal.util.guava.Optional;
@@ -38,6 +43,8 @@ import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -55,7 +62,8 @@ public class DbusSignalImpl implements Signal {
     private final String objectPath;
 
     private DBusPath thisDevice;
-    private final List<DBusPath> devices = new ArrayList<>();
+    private final List<StructDevice> devices = new ArrayList<>();
+    private final List<StructGroup> groups = new ArrayList<>();
 
     public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) {
         this.m = m;
@@ -65,10 +73,12 @@ public class DbusSignalImpl implements Signal {
 
     public void initObjects() {
         updateDevices();
+        updateGroups();
     }
 
     public void close() {
         unExportDevices();
+        unExportGroups();
     }
 
     @Override
@@ -81,6 +91,18 @@ public class DbusSignalImpl implements Signal {
         return m.getSelfNumber();
     }
 
+    @Override
+    public void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException {
+        final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", "");
+
+        try {
+            m.submitRateLimitRecaptchaChallenge(challenge, captcha);
+        } catch (IOException e) {
+            throw new IOErrorException("Submit challenge error: " + e.getMessage(), e);
+        }
+
+    }
+
     @Override
     public void addDevice(String uri) {
         try {
@@ -97,45 +119,19 @@ public class DbusSignalImpl implements Signal {
     @Override
     public DBusPath getDevice(long deviceId) {
         updateDevices();
-        return new DBusPath(getDeviceObjectPath(objectPath, deviceId));
+        final var deviceOptional = devices.stream().filter(g -> g.getId().equals(deviceId)).findFirst();
+        if (deviceOptional.isEmpty()) {
+            throw new Error.DeviceNotFound("Device not found");
+        }
+        return deviceOptional.get().getObjectPath();
     }
 
     @Override
-    public List<DBusPath> listDevices() {
+    public List<StructDevice> listDevices() {
         updateDevices();
         return this.devices;
     }
 
-    private void updateDevices() {
-        List<org.asamk.signal.manager.api.Device> 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 @@ public class DbusSignalImpl implements Signal {
                             .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 @@ public class DbusSignalImpl implements Signal {
                     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 @@ public class DbusSignalImpl implements Signal {
         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 @@ public class DbusSignalImpl implements Signal {
                     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) {
@@ -320,8 +316,8 @@ public class DbusSignalImpl implements Signal {
         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 +331,7 @@ public class DbusSignalImpl implements Signal {
     public void sendEndSessionMessage(final List<String> 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 +342,8 @@ public class DbusSignalImpl implements Signal {
         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 +367,8 @@ public class DbusSignalImpl implements Signal {
                     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 +419,8 @@ public class DbusSignalImpl implements Signal {
     public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
         try {
             m.setGroupBlocked(getGroupId(groupId), blocked);
+        } catch (NotMasterDeviceException e) {
+            throw new Error.Failure("This command doesn't work on linked devices.");
         } catch (GroupNotFoundException e) {
             throw new Error.GroupNotFound(e.getMessage());
         } catch (IOException e) {
@@ -435,18 +433,34 @@ public class DbusSignalImpl implements Signal {
         var groups = m.getGroups();
         var ids = new ArrayList<byte[]>(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<StructGroup> 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 +470,18 @@ public class DbusSignalImpl implements Signal {
         if (group == null) {
             return List.of();
         } else {
-            return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
+            final var members = group.members();
+            return getRecipientStrings(members);
         }
     }
 
+    @Override
+    public byte[] createGroup(
+            final String name, final List<String> 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<String> members, String avatar) {
         try {
@@ -469,25 +491,17 @@ public class DbusSignalImpl implements Signal {
             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 +613,8 @@ public class DbusSignalImpl implements Signal {
     // all numbers the system knows
     @Override
     public List<String> 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 +633,7 @@ public class DbusSignalImpl implements Signal {
         }
         // 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 +779,10 @@ public class DbusSignalImpl implements Signal {
         throw new Error.Failure(message.toString());
     }
 
+    private static List<String> getRecipientStrings(final Set<RecipientAddress> members) {
+        return members.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
+    }
+
     private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
             final Collection<String> recipientStrings, final String localNumber
     ) throws DBusExecutionException {
@@ -801,35 +819,101 @@ public class DbusSignalImpl implements Signal {
         return name.isEmpty() ? null : name;
     }
 
+    private String emptyIfNull(final String string) {
+        return string == null ? "" : string;
+    }
+
     private static String getDeviceObjectPath(String basePath, long deviceId) {
         return basePath + "/Devices/" + deviceId;
     }
 
+    private void updateDevices() {
+        List<org.asamk.signal.manager.api.Device> 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<org.asamk.signal.manager.api.Group> 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 +933,168 @@ public class DbusSignalImpl implements Signal {
             }
         }
     }
+
+    public class DbusSignalGroupImpl extends DbusProperties implements Signal.Group {
+
+        private final GroupId groupId;
+
+        public DbusSignalGroupImpl(final GroupId groupId) {
+            this.groupId = groupId;
+            super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group",
+                    List.of(new DbusProperty<>("Id", groupId::serialize),
+                            new DbusProperty<>("Name", () -> emptyIfNull(getGroup().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<String> recipients) throws Error.Failure {
+            final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+            updateGroup(UpdateGroup.newBuilder().withMembers(memberIdentifiers).build());
+        }
+
+        @Override
+        public void removeMembers(final List<String> recipients) throws Error.Failure {
+            final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+            updateGroup(UpdateGroup.newBuilder().withRemoveMembers(memberIdentifiers).build());
+        }
+
+        @Override
+        public void addAdmins(final List<String> recipients) throws Error.Failure {
+            final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+            updateGroup(UpdateGroup.newBuilder().withAdmins(memberIdentifiers).build());
+        }
+
+        @Override
+        public void removeAdmins(final List<String> 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());
+            }
+        }
+    }
 }
index a96fc5341e58403a61b96d12448290dfa6f305af..b78722d1249a87ef1da1e2549b5a8a146349b6eb 100644 (file)
@@ -1,43 +1,25 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
 
-class JsonAttachment {
-
-    @JsonProperty
-    final String contentType;
-
-    @JsonProperty
-    final String filename;
-
-    @JsonProperty
-    final String id;
-
-    @JsonProperty
-    final Long size;
-
-    JsonAttachment(SignalServiceAttachment attachment) {
-        this.contentType = attachment.getContentType();
+record JsonAttachment(String contentType, String filename, String id, Long size) {
 
+    static JsonAttachment from(SignalServiceAttachment attachment) {
         if (attachment.isPointer()) {
             final var pointer = attachment.asPointer();
-            this.id = pointer.getRemoteId().toString();
-            this.filename = pointer.getFileName().orNull();
-            this.size = pointer.getSize().transform(Integer::longValue).orNull();
+            final var id = pointer.getRemoteId().toString();
+            final var filename = pointer.getFileName().orNull();
+            final var size = pointer.getSize().transform(Integer::longValue).orNull();
+            return new JsonAttachment(attachment.getContentType(), filename, id, size);
         } else {
             final var stream = attachment.asStream();
-            this.id = null;
-            this.filename = stream.getFileName().orNull();
-            this.size = stream.getLength();
+            final var filename = stream.getFileName().orNull();
+            final var size = stream.getLength();
+            return new JsonAttachment(attachment.getContentType(), filename, null, size);
         }
     }
 
-    JsonAttachment(String filename) {
-        this.filename = filename;
-        this.contentType = null;
-        this.id = null;
-        this.size = null;
+    static JsonAttachment from(String filename) {
+        return new JsonAttachment(filename, null, null, null);
     }
 }
index 885c38a40fc04a04b3aaf44546837035715ab076..e30aeafa40ea9c109d51a016beb81481624414f7 100644 (file)
@@ -1,7 +1,6 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
 import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
@@ -12,33 +11,19 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
 
 import java.util.List;
 
-class JsonCallMessage {
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final OfferMessage offerMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final AnswerMessage answerMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final BusyMessage busyMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final HangupMessage hangupMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<IceUpdateMessage> iceUpdateMessages;
-
-    JsonCallMessage(SignalServiceCallMessage callMessage) {
-        this.offerMessage = callMessage.getOfferMessage().orNull();
-        this.answerMessage = callMessage.getAnswerMessage().orNull();
-        this.busyMessage = callMessage.getBusyMessage().orNull();
-        this.hangupMessage = callMessage.getHangupMessage().orNull();
-        this.iceUpdateMessages = callMessage.getIceUpdateMessages().orNull();
+record JsonCallMessage(
+        @JsonInclude(JsonInclude.Include.NON_NULL) OfferMessage offerMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) AnswerMessage answerMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) BusyMessage busyMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) HangupMessage hangupMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<IceUpdateMessage> iceUpdateMessages
+) {
+
+    static JsonCallMessage from(SignalServiceCallMessage callMessage) {
+        return new JsonCallMessage(callMessage.getOfferMessage().orNull(),
+                callMessage.getAnswerMessage().orNull(),
+                callMessage.getBusyMessage().orNull(),
+                callMessage.getHangupMessage().orNull(),
+                callMessage.getIceUpdateMessages().orNull());
     }
 }
index 712dd4f39852231b9637af95b15de14994b478fd..6ffd355b6ab3d616be306066d7dea7d29a05ee5f 100644 (file)
@@ -1,48 +1,29 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.asamk.signal.util.Util;
 import org.whispersystems.signalservice.api.messages.shared.SharedContact;
 
-public class JsonContactAddress {
-
-    @JsonProperty
-    private final SharedContact.PostalAddress.Type type;
-
-    @JsonProperty
-    private final String label;
-
-    @JsonProperty
-    private final String street;
-
-    @JsonProperty
-    private final String pobox;
-
-    @JsonProperty
-    private final String neighborhood;
-
-    @JsonProperty
-    private final String city;
-
-    @JsonProperty
-    private final String region;
-
-    @JsonProperty
-    private final String postcode;
-
-    @JsonProperty
-    private final String country;
-
-    public JsonContactAddress(SharedContact.PostalAddress address) {
-        type = address.getType();
-        label = Util.getStringIfNotBlank(address.getLabel());
-        street = Util.getStringIfNotBlank(address.getStreet());
-        pobox = Util.getStringIfNotBlank(address.getPobox());
-        neighborhood = Util.getStringIfNotBlank(address.getNeighborhood());
-        city = Util.getStringIfNotBlank(address.getCity());
-        region = Util.getStringIfNotBlank(address.getRegion());
-        postcode = Util.getStringIfNotBlank(address.getPostcode());
-        country = Util.getStringIfNotBlank(address.getCountry());
+public record JsonContactAddress(
+        SharedContact.PostalAddress.Type type,
+        String label,
+        String street,
+        String pobox,
+        String neighborhood,
+        String city,
+        String region,
+        String postcode,
+        String country
+) {
+
+    static JsonContactAddress from(SharedContact.PostalAddress address) {
+        return new JsonContactAddress(address.getType(),
+                Util.getStringIfNotBlank(address.getLabel()),
+                Util.getStringIfNotBlank(address.getStreet()),
+                Util.getStringIfNotBlank(address.getPobox()),
+                Util.getStringIfNotBlank(address.getNeighborhood()),
+                Util.getStringIfNotBlank(address.getCity()),
+                Util.getStringIfNotBlank(address.getRegion()),
+                Util.getStringIfNotBlank(address.getPostcode()),
+                Util.getStringIfNotBlank(address.getCountry()));
     }
 }
index 3ed55f6f1a9a3c535f05adae00e9821f894269cb..1bf53d0415b338821d7d231353abc38410f56fd4 100644 (file)
@@ -1,19 +1,10 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.whispersystems.signalservice.api.messages.shared.SharedContact;
 
-public class JsonContactAvatar {
-
-    @JsonProperty
-    private final JsonAttachment attachment;
-
-    @JsonProperty
-    private final boolean isProfile;
+public record JsonContactAvatar(JsonAttachment attachment, boolean isProfile) {
 
-    public JsonContactAvatar(SharedContact.Avatar avatar) {
-        attachment = new JsonAttachment(avatar.getAttachment());
-        isProfile = avatar.isProfile();
+    static JsonContactAvatar from(SharedContact.Avatar avatar) {
+        return new JsonContactAvatar(JsonAttachment.from(avatar.getAttachment()), avatar.isProfile());
     }
 }
index 070cfb72745495282be0c52f37f445f3df441cc6..ea6014c153f1f9f50add15f90fb196e393827e3c 100644 (file)
@@ -1,24 +1,11 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.asamk.signal.util.Util;
 import org.whispersystems.signalservice.api.messages.shared.SharedContact;
 
-public class JsonContactEmail {
-
-    @JsonProperty
-    private final String value;
-
-    @JsonProperty
-    private final SharedContact.Email.Type type;
-
-    @JsonProperty
-    private final String label;
+public record JsonContactEmail(String value, SharedContact.Email.Type type, String label) {
 
-    public JsonContactEmail(SharedContact.Email email) {
-        value = email.getValue();
-        type = email.getType();
-        label = Util.getStringIfNotBlank(email.getLabel());
+    static JsonContactEmail from(SharedContact.Email email) {
+        return new JsonContactEmail(email.getValue(), email.getType(), Util.getStringIfNotBlank(email.getLabel()));
     }
 }
index 9da2782504b98cc59bafd1fad72cd346db325700..9c7af32bcc66f114f0489205f2030f4fa24c54d5 100644 (file)
@@ -1,36 +1,18 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.asamk.signal.util.Util;
 import org.whispersystems.signalservice.api.messages.shared.SharedContact;
 
-public class JsonContactName {
-
-    @JsonProperty
-    private final String display;
-
-    @JsonProperty
-    private final String given;
-
-    @JsonProperty
-    private final String family;
-
-    @JsonProperty
-    private final String prefix;
-
-    @JsonProperty
-    private final String suffix;
-
-    @JsonProperty
-    private final String middle;
-
-    public JsonContactName(SharedContact.Name name) {
-        display = Util.getStringIfNotBlank(name.getDisplay());
-        given = Util.getStringIfNotBlank(name.getGiven());
-        family = Util.getStringIfNotBlank(name.getFamily());
-        prefix = Util.getStringIfNotBlank(name.getPrefix());
-        suffix = Util.getStringIfNotBlank(name.getSuffix());
-        middle = Util.getStringIfNotBlank(name.getMiddle());
+public record JsonContactName(
+        String display, String given, String family, String prefix, String suffix, String middle
+) {
+
+    static JsonContactName from(SharedContact.Name name) {
+        return new JsonContactName(Util.getStringIfNotBlank(name.getDisplay()),
+                Util.getStringIfNotBlank(name.getGiven()),
+                Util.getStringIfNotBlank(name.getFamily()),
+                Util.getStringIfNotBlank(name.getPrefix()),
+                Util.getStringIfNotBlank(name.getSuffix()),
+                Util.getStringIfNotBlank(name.getMiddle()));
     }
 }
index fce75843e3ee041e4cd9838154c5fbacd5c507af..6c23339ef21495e878935a00d29b2d2a84038b7b 100644 (file)
@@ -1,24 +1,11 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.asamk.signal.util.Util;
 import org.whispersystems.signalservice.api.messages.shared.SharedContact;
 
-public class JsonContactPhone {
-
-    @JsonProperty
-    private final String value;
-
-    @JsonProperty
-    private final SharedContact.Phone.Type type;
-
-    @JsonProperty
-    private final String label;
+public record JsonContactPhone(String value, SharedContact.Phone.Type type, String label) {
 
-    public JsonContactPhone(SharedContact.Phone phone) {
-        value = phone.getValue();
-        type = phone.getType();
-        label = Util.getStringIfNotBlank(phone.getLabel());
+    static JsonContactPhone from(SharedContact.Phone phone) {
+        return new JsonContactPhone(phone.getValue(), phone.getType(), Util.getStringIfNotBlank(phone.getLabel()));
     }
 }
index 6dbda9783e5b081eac7533e94b6c2b8cc7b5beb2..1e898175ea1b4a09702c8616cd6dfd4251e2316f 100644 (file)
@@ -1,7 +1,6 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.asamk.Signal;
 import org.asamk.signal.manager.Manager;
@@ -10,136 +9,124 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 import java.util.List;
 import java.util.stream.Collectors;
 
-class JsonDataMessage {
-
-    @JsonProperty
-    final long timestamp;
-
-    @JsonProperty
-    final String message;
-
-    @JsonProperty
-    final Integer expiresInSeconds;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final Boolean viewOnce;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonReaction reaction;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonQuote quote;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<JsonMention> mentions;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<JsonAttachment> attachments;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonSticker sticker;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonRemoteDelete remoteDelete;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<JsonSharedContact> contacts;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonGroupInfo groupInfo;
-
-    JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) {
-        this.timestamp = dataMessage.getTimestamp();
+record JsonDataMessage(
+        long timestamp,
+        String message,
+        Integer expiresInSeconds,
+        @JsonInclude(JsonInclude.Include.NON_NULL) Boolean viewOnce,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonReaction reaction,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonQuote quote,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonMention> mentions,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonAttachment> attachments,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonSticker sticker,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonRemoteDelete remoteDelete,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonSharedContact> contacts,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonGroupInfo groupInfo
+) {
+
+    static JsonDataMessage from(SignalServiceDataMessage dataMessage, Manager m) {
+        final var timestamp = dataMessage.getTimestamp();
+        final JsonGroupInfo groupInfo;
         if (dataMessage.getGroupContext().isPresent()) {
             final var groupContext = dataMessage.getGroupContext().get();
             if (groupContext.getGroupV1().isPresent()) {
-                var groupInfo = groupContext.getGroupV1().get();
-                this.groupInfo = new JsonGroupInfo(groupInfo);
+                var group = groupContext.getGroupV1().get();
+                groupInfo = JsonGroupInfo.from(group);
             } else if (groupContext.getGroupV2().isPresent()) {
-                var groupInfo = groupContext.getGroupV2().get();
-                this.groupInfo = new JsonGroupInfo(groupInfo);
+                var group = groupContext.getGroupV2().get();
+                groupInfo = JsonGroupInfo.from(group);
             } else {
-                this.groupInfo = null;
+                groupInfo = null;
             }
         } else {
-            this.groupInfo = null;
+            groupInfo = null;
         }
-        this.message = dataMessage.getBody().orNull();
-        this.expiresInSeconds = dataMessage.getExpiresInSeconds();
-        this.viewOnce = dataMessage.isViewOnce();
-        this.reaction = dataMessage.getReaction().isPresent()
-                ? new JsonReaction(dataMessage.getReaction().get(), m)
-                : null;
-        this.quote = dataMessage.getQuote().isPresent() ? new JsonQuote(dataMessage.getQuote().get(), m) : null;
+        final var message = dataMessage.getBody().orNull();
+        final var expiresInSeconds = dataMessage.getExpiresInSeconds();
+        final var viewOnce = dataMessage.isViewOnce();
+        final var reaction = dataMessage.getReaction().isPresent() ? JsonReaction.from(dataMessage.getReaction().get(),
+                m) : null;
+        final var quote = dataMessage.getQuote().isPresent() ? JsonQuote.from(dataMessage.getQuote().get(), m) : null;
+        final List<JsonMention> mentions;
         if (dataMessage.getMentions().isPresent()) {
-            this.mentions = dataMessage.getMentions()
+            mentions = dataMessage.getMentions()
                     .get()
                     .stream()
-                    .map(mention -> new JsonMention(mention, m))
+                    .map(mention -> JsonMention.from(mention, m))
                     .collect(Collectors.toList());
         } else {
-            this.mentions = List.of();
+            mentions = List.of();
         }
-        remoteDelete = dataMessage.getRemoteDelete().isPresent() ? new JsonRemoteDelete(dataMessage.getRemoteDelete()
-                .get()) : null;
+        final var remoteDelete = dataMessage.getRemoteDelete().isPresent()
+                ? JsonRemoteDelete.from(dataMessage.getRemoteDelete().get())
+                : null;
+        final List<JsonAttachment> attachments;
         if (dataMessage.getAttachments().isPresent()) {
-            this.attachments = dataMessage.getAttachments()
+            attachments = dataMessage.getAttachments()
                     .get()
                     .stream()
-                    .map(JsonAttachment::new)
+                    .map(JsonAttachment::from)
                     .collect(Collectors.toList());
         } else {
-            this.attachments = List.of();
+            attachments = List.of();
         }
-        this.sticker = dataMessage.getSticker().isPresent() ? new JsonSticker(dataMessage.getSticker().get()) : null;
+        final var sticker = dataMessage.getSticker().isPresent()
+                ? JsonSticker.from(dataMessage.getSticker().get())
+                : null;
 
+        final List<JsonSharedContact> contacts;
         if (dataMessage.getSharedContacts().isPresent()) {
-            this.contacts = dataMessage.getSharedContacts()
+            contacts = dataMessage.getSharedContacts()
                     .get()
                     .stream()
-                    .map(JsonSharedContact::new)
+                    .map(JsonSharedContact::from)
                     .collect(Collectors.toList());
         } else {
-            this.contacts = List.of();
+            contacts = List.of();
         }
+        return new JsonDataMessage(timestamp,
+                message,
+                expiresInSeconds,
+                viewOnce,
+                reaction,
+                quote,
+                mentions,
+                attachments,
+                sticker,
+                remoteDelete,
+                contacts,
+                groupInfo);
     }
 
-    public JsonDataMessage(Signal.MessageReceived messageReceived) {
-        timestamp = messageReceived.getTimestamp();
-        message = messageReceived.getMessage();
-        groupInfo = messageReceived.getGroupId().length > 0 ? new JsonGroupInfo(messageReceived.getGroupId()) : null;
-        expiresInSeconds = null;
-        viewOnce = null;
-        remoteDelete = null;
-        reaction = null;    // TODO Replace these 5 with the proper commands
-        quote = null;
-        mentions = null;
-        sticker = null;
-        contacts = null;
-        attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
+    static JsonDataMessage from(Signal.MessageReceived messageReceived) {
+        return new JsonDataMessage(messageReceived.getTimestamp(),
+                messageReceived.getMessage(),
+                // TODO Replace these with the proper commands
+                null,
+                null,
+                null,
+                null,
+                null,
+                messageReceived.getAttachments().stream().map(JsonAttachment::from).collect(Collectors.toList()),
+                null,
+                null,
+                null,
+                messageReceived.getGroupId().length > 0 ? JsonGroupInfo.from(messageReceived.getGroupId()) : null);
     }
 
-    public JsonDataMessage(Signal.SyncMessageReceived messageReceived) {
-        timestamp = messageReceived.getTimestamp();
-        message = messageReceived.getMessage();
-        groupInfo = messageReceived.getGroupId().length > 0 ? new JsonGroupInfo(messageReceived.getGroupId()) : null;
-        expiresInSeconds = null;
-        viewOnce = null;
-        remoteDelete = null;
-        reaction = null;    // TODO Replace these 5 with the proper commands
-        quote = null;
-        mentions = null;
-        sticker = null;
-        contacts = null;
-        attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
+    static JsonDataMessage from(Signal.SyncMessageReceived messageReceived) {
+        return new JsonDataMessage(messageReceived.getTimestamp(),
+                messageReceived.getMessage(),
+                // TODO Replace these with the proper commands
+                null,
+                null,
+                null,
+                null,
+                null,
+                messageReceived.getAttachments().stream().map(JsonAttachment::from).collect(Collectors.toList()),
+                null,
+                null,
+                null,
+                messageReceived.getGroupId().length > 0 ? JsonGroupInfo.from(messageReceived.getGroupId()) : null);
     }
 }
index 45274dea3a4bbeaaca66d3de94d6e30b9be6c6eb..07f8a43101f463c06ce3e752b08842e23d50bd75 100644 (file)
@@ -1,17 +1,8 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
+public record JsonError(String message, String type) {
 
-public class JsonError {
-
-    @JsonProperty
-    final String message;
-
-    @JsonProperty
-    final String type;
-
-    public JsonError(Throwable exception) {
-        this.message = exception.getMessage();
-        this.type = exception.getClass().getSimpleName();
+    public static JsonError from(Throwable exception) {
+        return new JsonError(exception.getMessage(), exception.getClass().getSimpleName());
     }
 }
index de78186aa426eff16fcc167974c67f98ce9f6c27..2c1a5b801f8fb073afcc358a54b8a17c43e7d3e5 100644 (file)
@@ -1,7 +1,6 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.util.Util;
@@ -12,48 +11,32 @@ import java.util.Base64;
 import java.util.List;
 import java.util.stream.Collectors;
 
-class JsonGroupInfo {
-
-    @JsonProperty
-    final String groupId;
-
-    @JsonProperty
-    final String type;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final String name;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<String> members;
-
-    JsonGroupInfo(SignalServiceGroup groupInfo) {
-        this.groupId = Base64.getEncoder().encodeToString(groupInfo.getGroupId());
-        this.type = groupInfo.getType().toString();
-        this.name = groupInfo.getName().orNull();
-        if (groupInfo.getMembers().isPresent()) {
-            this.members = groupInfo.getMembers()
-                    .get()
-                    .stream()
-                    .map(Util::getLegacyIdentifier)
-                    .collect(Collectors.toList());
-        } else {
-            this.members = null;
-        }
+record JsonGroupInfo(
+        String groupId,
+        String type,
+        @JsonInclude(JsonInclude.Include.NON_NULL) String name,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<String> members
+) {
+
+    static JsonGroupInfo from(SignalServiceGroup groupInfo) {
+        return new JsonGroupInfo(Base64.getEncoder().encodeToString(groupInfo.getGroupId()),
+                groupInfo.getType().toString(),
+                groupInfo.getName().orNull(),
+                groupInfo.getMembers().isPresent() ? groupInfo.getMembers()
+                        .get()
+                        .stream()
+                        .map(Util::getLegacyIdentifier)
+                        .collect(Collectors.toList()) : null);
     }
 
-    JsonGroupInfo(SignalServiceGroupV2 groupInfo) {
-        this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64();
-        this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER";
-        this.members = null;
-        this.name = null;
+    static JsonGroupInfo from(SignalServiceGroupV2 groupInfo) {
+        return new JsonGroupInfo(GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64(),
+                groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER",
+                null,
+                null);
     }
 
-    JsonGroupInfo(byte[] groupId) {
-        this.groupId = Base64.getEncoder().encodeToString(groupId);
-        this.type = "DELIVER";
-        this.members = null;
-        this.name = null;
+    static JsonGroupInfo from(byte[] groupId) {
+        return new JsonGroupInfo(Base64.getEncoder().encodeToString(groupId), "DELIVER", null, null);
     }
 }
index 3c6f2eec091333721bd85ee12dd08dcafc547d35..66fcc1f1a72e725764d593d6c67d5177d2dc412f 100644 (file)
@@ -1,37 +1,19 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.asamk.signal.manager.Manager;
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 
 import static org.asamk.signal.util.Util.getLegacyIdentifier;
 
-public class JsonMention {
-
-    @JsonProperty
-    @Deprecated
-    final String name;
-
-    @JsonProperty
-    final String number;
-
-    @JsonProperty
-    final String uuid;
-
-    @JsonProperty
-    final int start;
-
-    @JsonProperty
-    final int length;
+public record JsonMention(@Deprecated String name, String number, String uuid, int start, int length) {
 
-    JsonMention(SignalServiceDataMessage.Mention mention, Manager m) {
+    static JsonMention from(SignalServiceDataMessage.Mention mention, Manager m) {
         final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid()));
-        this.name = getLegacyIdentifier(address);
-        this.number = address.getNumber().orNull();
-        this.uuid = address.getUuid().toString();
-        this.start = mention.getStart();
-        this.length = mention.getLength();
+        return new JsonMention(getLegacyIdentifier(address),
+                address.getNumber().orNull(),
+                address.getUuid().toString(),
+                mention.getStart(),
+                mention.getLength());
     }
 }
index e49e6125b77dee0c4a58ab4e88b8e9be328d34f7..f5d70d28a61d7bb2bdc62ae3993343651490b3d3 100644 (file)
@@ -1,7 +1,6 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.asamk.Signal;
 import org.asamk.signal.manager.Manager;
@@ -15,144 +14,133 @@ import java.util.List;
 
 import static org.asamk.signal.util.Util.getLegacyIdentifier;
 
-public class JsonMessageEnvelope {
-
-    @JsonProperty
-    @Deprecated
-    final String source;
-
-    @JsonProperty
-    final String sourceNumber;
-
-    @JsonProperty
-    final String sourceUuid;
-
-    @JsonProperty
-    final String sourceName;
-
-    @JsonProperty
-    final Integer sourceDevice;
-
-    @JsonProperty
-    final long timestamp;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonDataMessage dataMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonSyncMessage syncMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonCallMessage callMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonReceiptMessage receiptMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonTypingMessage typingMessage;
-
-    public JsonMessageEnvelope(
+public record JsonMessageEnvelope(
+        @Deprecated String source,
+        String sourceNumber,
+        String sourceUuid,
+        String sourceName,
+        Integer sourceDevice,
+        long timestamp,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonDataMessage dataMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonSyncMessage syncMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonCallMessage callMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonReceiptMessage receiptMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonTypingMessage typingMessage
+) {
+
+    public static JsonMessageEnvelope from(
             SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception, Manager m
     ) {
+        final String source;
+        final String sourceNumber;
+        final String sourceUuid;
+        final Integer sourceDevice;
         if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
-            var source = m.resolveSignalServiceAddress(envelope.getSourceAddress());
-            this.source = getLegacyIdentifier(source);
-            this.sourceNumber = source.getNumber().orNull();
-            this.sourceUuid = source.getUuid().toString();
-            this.sourceDevice = envelope.getSourceDevice();
+            final var sourceAddress = m.resolveSignalServiceAddress(envelope.getSourceAddress());
+            source = getLegacyIdentifier(sourceAddress);
+            sourceNumber = sourceAddress.getNumber().orNull();
+            sourceUuid = sourceAddress.getUuid().toString();
+            sourceDevice = envelope.getSourceDevice();
         } else if (envelope.isUnidentifiedSender() && content != null) {
-            final var source = m.resolveSignalServiceAddress(content.getSender());
-            this.source = getLegacyIdentifier(source);
-            this.sourceNumber = source.getNumber().orNull();
-            this.sourceUuid = source.getUuid().toString();
-            this.sourceDevice = content.getSenderDevice();
-        } else if (exception instanceof UntrustedIdentityException) {
-            var e = (UntrustedIdentityException) exception;
-            final var source = m.resolveSignalServiceAddress(e.getSender());
-            this.source = getLegacyIdentifier(source);
-            this.sourceNumber = source.getNumber().orNull();
-            this.sourceUuid = source.getUuid().toString();
-            this.sourceDevice = e.getSenderDevice();
+            final var sender = m.resolveSignalServiceAddress(content.getSender());
+            source = getLegacyIdentifier(sender);
+            sourceNumber = sender.getNumber().orNull();
+            sourceUuid = sender.getUuid().toString();
+            sourceDevice = content.getSenderDevice();
+        } else if (exception instanceof UntrustedIdentityException e) {
+            final var sender = m.resolveSignalServiceAddress(e.getSender());
+            source = getLegacyIdentifier(sender);
+            sourceNumber = sender.getNumber().orNull();
+            sourceUuid = sender.getUuid().toString();
+            sourceDevice = e.getSenderDevice();
         } else {
-            this.source = null;
-            this.sourceNumber = null;
-            this.sourceUuid = null;
-            this.sourceDevice = null;
+            source = null;
+            sourceNumber = null;
+            sourceUuid = null;
+            sourceDevice = null;
         }
         String name;
         try {
-            name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getSelfNumber()));
+            name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(source, m.getSelfNumber()));
         } catch (InvalidNumberException | NullPointerException e) {
             name = null;
         }
-        this.sourceName = name;
-        this.timestamp = envelope.getTimestamp();
+        final var sourceName = name;
+        final var timestamp = envelope.getTimestamp();
+        final JsonReceiptMessage receiptMessage;
         if (envelope.isReceipt()) {
-            this.receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
+            receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
         } else if (content != null && content.getReceiptMessage().isPresent()) {
-            this.receiptMessage = new JsonReceiptMessage(content.getReceiptMessage().get());
+            receiptMessage = JsonReceiptMessage.from(content.getReceiptMessage().get());
         } else {
-            this.receiptMessage = null;
+            receiptMessage = null;
         }
-        this.typingMessage = content != null && content.getTypingMessage().isPresent()
-                ? new JsonTypingMessage(content.getTypingMessage().get())
-                : null;
+        final var typingMessage = content != null && content.getTypingMessage().isPresent() ? JsonTypingMessage.from(
+                content.getTypingMessage().get()) : null;
 
-        this.dataMessage = content != null && content.getDataMessage().isPresent()
-                ? new JsonDataMessage(content.getDataMessage().get(), m)
+        final var dataMessage = content != null && content.getDataMessage().isPresent()
+                ? JsonDataMessage.from(content.getDataMessage().get(), m)
                 : null;
-        this.syncMessage = content != null && content.getSyncMessage().isPresent()
-                ? new JsonSyncMessage(content.getSyncMessage().get(), m)
+        final var syncMessage = content != null && content.getSyncMessage().isPresent()
+                ? JsonSyncMessage.from(content.getSyncMessage().get(), m)
                 : null;
-        this.callMessage = content != null && content.getCallMessage().isPresent()
-                ? new JsonCallMessage(content.getCallMessage().get())
+        final var callMessage = content != null && content.getCallMessage().isPresent()
+                ? JsonCallMessage.from(content.getCallMessage().get())
                 : null;
+
+        return new JsonMessageEnvelope(source,
+                sourceNumber,
+                sourceUuid,
+                sourceName,
+                sourceDevice,
+                timestamp,
+                dataMessage,
+                syncMessage,
+                callMessage,
+                receiptMessage,
+                typingMessage);
     }
 
-    public JsonMessageEnvelope(Signal.MessageReceived messageReceived) {
-        source = messageReceived.getSender();
-        sourceNumber = null;
-        sourceUuid = null;
-        sourceName = null;
-        sourceDevice = null;
-        timestamp = messageReceived.getTimestamp();
-        receiptMessage = null;
-        dataMessage = new JsonDataMessage(messageReceived);
-        syncMessage = null;
-        callMessage = null;
-        typingMessage = null;
+    public static JsonMessageEnvelope from(Signal.MessageReceived messageReceived) {
+        return new JsonMessageEnvelope(messageReceived.getSource(),
+                null,
+                null,
+                null,
+                null,
+                messageReceived.getTimestamp(),
+                JsonDataMessage.from(messageReceived),
+                null,
+                null,
+                null,
+                null);
     }
 
-    public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) {
-        source = receiptReceived.getSender();
-        sourceNumber = null;
-        sourceUuid = null;
-        sourceName = null;
-        sourceDevice = null;
-        timestamp = receiptReceived.getTimestamp();
-        receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
-        dataMessage = null;
-        syncMessage = null;
-        callMessage = null;
-        typingMessage = null;
+    public static JsonMessageEnvelope from(Signal.ReceiptReceived receiptReceived) {
+        return new JsonMessageEnvelope(receiptReceived.getSender(),
+                null,
+                null,
+                null,
+                null,
+                receiptReceived.getTimestamp(),
+                null,
+                null,
+                null,
+                JsonReceiptMessage.deliveryReceipt(receiptReceived.getTimestamp(),
+                        List.of(receiptReceived.getTimestamp())),
+                null);
     }
 
-    public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) {
-        source = messageReceived.getSource();
-        sourceNumber = null;
-        sourceUuid = null;
-        sourceName = null;
-        sourceDevice = null;
-        timestamp = messageReceived.getTimestamp();
-        receiptMessage = null;
-        dataMessage = null;
-        syncMessage = new JsonSyncMessage(messageReceived);
-        callMessage = null;
-        typingMessage = null;
+    public static JsonMessageEnvelope from(Signal.SyncMessageReceived messageReceived) {
+        return new JsonMessageEnvelope(messageReceived.getSource(),
+                null,
+                null,
+                null,
+                null,
+                messageReceived.getTimestamp(),
+                null,
+                JsonSyncMessage.from(messageReceived),
+                null,
+                null,
+                null);
     }
 }
index 73af895ae3c60c2050ee6ce9f080a1701f292d8e..f0d1831c5b9a7e0a6a78573e39356f5fc2ed32c1 100644 (file)
@@ -1,7 +1,6 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.asamk.signal.manager.Manager;
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@@ -12,55 +11,41 @@ import java.util.stream.Collectors;
 
 import static org.asamk.signal.util.Util.getLegacyIdentifier;
 
-public class JsonQuote {
-
-    @JsonProperty
-    final long id;
-
-    @JsonProperty
-    @Deprecated
-    final String author;
-
-    @JsonProperty
-    final String authorNumber;
-
-    @JsonProperty
-    final String authorUuid;
-
-    @JsonProperty
-    final String text;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<JsonMention> mentions;
-
-    @JsonProperty
-    final List<JsonQuotedAttachment> attachments;
-
-    JsonQuote(SignalServiceDataMessage.Quote quote, Manager m) {
-        this.id = quote.getId();
+public record JsonQuote(
+        long id,
+        @Deprecated String author,
+        String authorNumber,
+        String authorUuid,
+        String text,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonMention> mentions,
+        List<JsonQuotedAttachment> attachments
+) {
+
+    static JsonQuote from(SignalServiceDataMessage.Quote quote, Manager m) {
+        final var id = quote.getId();
         final var address = m.resolveSignalServiceAddress(quote.getAuthor());
-        this.author = getLegacyIdentifier(address);
-        this.authorNumber = address.getNumber().orNull();
-        this.authorUuid = address.getUuid().toString();
-        this.text = quote.getText();
+        final var author = getLegacyIdentifier(address);
+        final var authorNumber = address.getNumber().orNull();
+        final var authorUuid = address.getUuid().toString();
+        final var text = quote.getText();
 
+        final List<JsonMention> mentions;
         if (quote.getMentions() != null && quote.getMentions().size() > 0) {
-            this.mentions = quote.getMentions()
+            mentions = quote.getMentions()
                     .stream()
-                    .map(quotedMention -> new JsonMention(quotedMention, m))
+                    .map(quotedMention -> JsonMention.from(quotedMention, m))
                     .collect(Collectors.toList());
         } else {
-            this.mentions = null;
+            mentions = null;
         }
 
+        final List<JsonQuotedAttachment> attachments;
         if (quote.getAttachments().size() > 0) {
-            this.attachments = quote.getAttachments()
-                    .stream()
-                    .map(JsonQuotedAttachment::new)
-                    .collect(Collectors.toList());
+            attachments = quote.getAttachments().stream().map(JsonQuotedAttachment::from).collect(Collectors.toList());
         } else {
-            this.attachments = new ArrayList<>();
+            attachments = new ArrayList<>();
         }
+
+        return new JsonQuote(id, author, authorNumber, authorUuid, text, mentions, attachments);
     }
 }
index f3f809dc1fadb3d87e58c8863dd1a34de47d3399..a72ba7c6d285692f8586afb9b9c4e401e110bb1a 100644 (file)
@@ -1,29 +1,22 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 
-public class JsonQuotedAttachment {
+public record JsonQuotedAttachment(
+        String contentType, String filename, @JsonInclude(JsonInclude.Include.NON_NULL) JsonAttachment thumbnail
+) {
 
-    @JsonProperty
-    final String contentType;
-
-    @JsonProperty
-    final String filename;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonAttachment thumbnail;
-
-    JsonQuotedAttachment(SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment) {
-        contentType = quotedAttachment.getContentType();
-        filename = quotedAttachment.getFileName();
+    static JsonQuotedAttachment from(SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment) {
+        final var contentType = quotedAttachment.getContentType();
+        final var filename = quotedAttachment.getFileName();
+        final JsonAttachment thumbnail;
         if (quotedAttachment.getThumbnail() != null) {
-            thumbnail = new JsonAttachment(quotedAttachment.getThumbnail());
+            thumbnail = JsonAttachment.from(quotedAttachment.getThumbnail());
         } else {
             thumbnail = null;
         }
+        return new JsonQuotedAttachment(contentType, filename, thumbnail);
     }
 }
index cc80ee84eb3ac89dde1c459fb69371a325a079fc..9dc645919aa4ec061769423c57d40e974fd2e840 100644 (file)
@@ -1,40 +1,32 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.asamk.signal.manager.Manager;
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction;
 
 import static org.asamk.signal.util.Util.getLegacyIdentifier;
 
-public class JsonReaction {
-
-    @JsonProperty
-    final String emoji;
-
-    @JsonProperty
-    @Deprecated
-    final String targetAuthor;
-
-    @JsonProperty
-    final String targetAuthorNumber;
-
-    @JsonProperty
-    final String targetAuthorUuid;
-
-    @JsonProperty
-    final long targetSentTimestamp;
-
-    @JsonProperty
-    final boolean isRemove;
-
-    JsonReaction(Reaction reaction, Manager m) {
-        this.emoji = reaction.getEmoji();
+public record JsonReaction(
+        String emoji,
+        @Deprecated String targetAuthor,
+        String targetAuthorNumber,
+        String targetAuthorUuid,
+        long targetSentTimestamp,
+        boolean isRemove
+) {
+
+    static JsonReaction from(Reaction reaction, Manager m) {
+        final var emoji = reaction.getEmoji();
         final var address = m.resolveSignalServiceAddress(reaction.getTargetAuthor());
-        this.targetAuthor = getLegacyIdentifier(address);
-        this.targetAuthorNumber = address.getNumber().orNull();
-        this.targetAuthorUuid = address.getUuid().toString();
-        this.targetSentTimestamp = reaction.getTargetSentTimestamp();
-        this.isRemove = reaction.isRemove();
+        final var targetAuthor = getLegacyIdentifier(address);
+        final var targetAuthorNumber = address.getNumber().orNull();
+        final var targetAuthorUuid = address.getUuid().toString();
+        final var targetSentTimestamp = reaction.getTargetSentTimestamp();
+        final var isRemove = reaction.isRemove();
+        return new JsonReaction(emoji,
+                targetAuthor,
+                targetAuthorNumber,
+                targetAuthorUuid,
+                targetSentTimestamp,
+                isRemove);
     }
 }
index 15e2cf434f6a41f0b44d9c2d7eaf0facc81b5808..1199c790270054aa08293ded2c116e825820f942 100644 (file)
@@ -1,47 +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<Long> 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, boolean isViewed, List<Long> timestamps) {
 
-    private JsonReceiptMessage(
-            final long when, final boolean isDelivery, final boolean isRead, final boolean isViewed, final List<Long> 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, isViewed, timestamps);
     }
 
     static JsonReceiptMessage deliveryReceipt(final long when, final List<Long> timestamps) {
-        return new JsonReceiptMessage(when, true, false, false, timestamps);
+        return new JsonReceiptMessage(when, true, false, false, false, timestamps);
     }
 }
index a498a0e5b316bbec1b281570ce083c62b991dfed..611d1141eb828c5d565d9a684ac01d0156a318c6 100644 (file)
@@ -1,15 +1,10 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 
-class JsonRemoteDelete {
-
-    @JsonProperty
-    final long timestamp;
+record JsonRemoteDelete(long timestamp) {
 
-    JsonRemoteDelete(SignalServiceDataMessage.RemoteDelete remoteDelete) {
-        this.timestamp = remoteDelete.getTargetSentTimestamp();
+    static JsonRemoteDelete from(SignalServiceDataMessage.RemoteDelete remoteDelete) {
+        return new JsonRemoteDelete(remoteDelete.getTargetSentTimestamp());
     }
 }
index ea15b5748ff379e76faf801d661b03abc09bc88b..f52be3eb94467d299f1d94a6acf3fd65952212c4 100644 (file)
@@ -1,62 +1,45 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.whispersystems.signalservice.api.messages.shared.SharedContact;
 
 import java.util.List;
 import java.util.stream.Collectors;
 
-public class JsonSharedContact {
-
-    @JsonProperty
-    final JsonContactName name;
-
-    @JsonProperty
-    final JsonContactAvatar avatar;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<JsonContactPhone> phone;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<JsonContactEmail> email;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<JsonContactAddress> address;
-
-    @JsonProperty
-    final String organization;
-
-    public JsonSharedContact(SharedContact contact) {
-        name = new JsonContactName(contact.getName());
-        if (contact.getAvatar().isPresent()) {
-            avatar = new JsonContactAvatar(contact.getAvatar().get());
-        } else {
-            avatar = null;
-        }
-
-        if (contact.getPhone().isPresent()) {
-            phone = contact.getPhone().get().stream().map(JsonContactPhone::new).collect(Collectors.toList());
-        } else {
-            phone = null;
-        }
-
-        if (contact.getEmail().isPresent()) {
-            email = contact.getEmail().get().stream().map(JsonContactEmail::new).collect(Collectors.toList());
-        } else {
-            email = null;
-        }
-
-        if (contact.getAddress().isPresent()) {
-            address = contact.getAddress().get().stream().map(JsonContactAddress::new).collect(Collectors.toList());
-        } else {
-            address = null;
-        }
-
-        organization = contact.getOrganization().orNull();
+public record JsonSharedContact(
+        JsonContactName name,
+        JsonContactAvatar avatar,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonContactPhone> phone,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonContactEmail> email,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonContactAddress> address,
+        String organization
+) {
+
+    static JsonSharedContact from(SharedContact contact) {
+        final var name = JsonContactName.from(contact.getName());
+        final var avatar = contact.getAvatar().isPresent() ? JsonContactAvatar.from(contact.getAvatar().get()) : null;
+
+        final var phone = contact.getPhone().isPresent() ? contact.getPhone()
+                .get()
+                .stream()
+                .map(JsonContactPhone::from)
+                .collect(Collectors.toList()) : null;
+
+        final var email = contact.getEmail().isPresent() ? contact.getEmail()
+                .get()
+                .stream()
+                .map(JsonContactEmail::from)
+                .collect(Collectors.toList()) : null;
+
+        final var address = contact.getAddress().isPresent() ? contact.getAddress()
+                .get()
+                .stream()
+                .map(JsonContactAddress::from)
+                .collect(Collectors.toList()) : null;
+
+        final var organization = contact.getOrganization().orNull();
+
+        return new JsonSharedContact(name, avatar, phone, email, address, organization);
     }
 }
index e56ddf3fef5d4d552d6f1f1006660c6565d16e12..9278ee3779735b7101549dd8e303d334dc820933 100644 (file)
@@ -1,25 +1,15 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
 
 import java.util.Base64;
 
-public class JsonSticker {
-
-    @JsonProperty
-    final String packId;
-
-    @JsonProperty
-    final String packKey;
-
-    @JsonProperty
-    final int stickerId;
+public record JsonSticker(String packId, String packKey, int stickerId) {
 
-    public JsonSticker(SignalServiceDataMessage.Sticker sticker) {
-        this.packId = Base64.getEncoder().encodeToString(sticker.getPackId());
-        this.packKey = Base64.getEncoder().encodeToString(sticker.getPackKey());
-        this.stickerId = sticker.getStickerId();
+    static JsonSticker from(SignalServiceDataMessage.Sticker sticker) {
+        final var packId = Base64.getEncoder().encodeToString(sticker.getPackId());
+        final var packKey = Base64.getEncoder().encodeToString(sticker.getPackKey());
+        final var stickerId = sticker.getStickerId();
+        return new JsonSticker(packId, packKey, stickerId);
     }
 }
index e2c92bac2e8a2a4c17d7ddcf3b8fb95a980f42e4..98139c84bd68811d074a92f3d52f38e86356910f 100644 (file)
@@ -1,6 +1,6 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
 
 import org.asamk.Signal;
 import org.asamk.signal.manager.Manager;
@@ -8,37 +8,30 @@ import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptM
 
 import static org.asamk.signal.util.Util.getLegacyIdentifier;
 
-class JsonSyncDataMessage extends JsonDataMessage {
-
-    @JsonProperty
-    @Deprecated
-    final String destination;
-
-    @JsonProperty
-    final String destinationNumber;
-
-    @JsonProperty
-    final String destinationUuid;
-
-    JsonSyncDataMessage(SentTranscriptMessage transcriptMessage, Manager m) {
-        super(transcriptMessage.getMessage(), m);
+record JsonSyncDataMessage(
+        @Deprecated String destination,
+        String destinationNumber,
+        String destinationUuid,
+        @JsonUnwrapped JsonDataMessage dataMessage
+) {
 
+    static JsonSyncDataMessage from(SentTranscriptMessage transcriptMessage, Manager m) {
         if (transcriptMessage.getDestination().isPresent()) {
             final var address = transcriptMessage.getDestination().get();
-            this.destination = getLegacyIdentifier(address);
-            this.destinationNumber = address.getNumber().orNull();
-            this.destinationUuid = address.getUuid().toString();
+            return new JsonSyncDataMessage(getLegacyIdentifier(address),
+                    address.getNumber().orNull(),
+                    address.getUuid().toString(),
+                    JsonDataMessage.from(transcriptMessage.getMessage(), m));
+
         } else {
-            this.destination = null;
-            this.destinationNumber = null;
-            this.destinationUuid = null;
+            return new JsonSyncDataMessage(null, null, null, JsonDataMessage.from(transcriptMessage.getMessage(), m));
         }
     }
 
-    JsonSyncDataMessage(Signal.SyncMessageReceived messageReceived) {
-        super(messageReceived);
-        this.destination = messageReceived.getDestination();
-        this.destinationNumber = null;
-        this.destinationUuid = null;
+    static JsonSyncDataMessage from(Signal.SyncMessageReceived messageReceived) {
+        return new JsonSyncDataMessage(messageReceived.getDestination(),
+                null,
+                null,
+                JsonDataMessage.from(messageReceived));
     }
 }
index 5c951f0f16c0f60623c962215600cb7f6b99a596..2628844f1dcfd73cfe69a10b31154b9cc3eefdf0 100644 (file)
@@ -1,7 +1,6 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.asamk.Signal;
 import org.asamk.signal.manager.Manager;
@@ -18,76 +17,72 @@ enum JsonSyncMessageType {
     REQUEST_SYNC
 }
 
-class JsonSyncMessage {
+record JsonSyncMessage(
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonSyncDataMessage sentMessage,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<String> blockedNumbers,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<String> blockedGroupIds,
+        @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonSyncReadMessage> readMessages,
+        @JsonInclude(JsonInclude.Include.NON_NULL) JsonSyncMessageType type
+) {
 
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonSyncDataMessage sentMessage;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<String> blockedNumbers;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<String> blockedGroupIds;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final List<JsonSyncReadMessage> readMessages;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final JsonSyncMessageType type;
+    JsonSyncMessage(
+            final JsonSyncDataMessage sentMessage,
+            final List<String> blockedNumbers,
+            final List<String> blockedGroupIds,
+            final List<JsonSyncReadMessage> readMessages,
+            final JsonSyncMessageType type
+    ) {
+        this.sentMessage = sentMessage;
+        this.blockedNumbers = blockedNumbers;
+        this.blockedGroupIds = blockedGroupIds;
+        this.readMessages = readMessages;
+        this.type = type;
+    }
 
-    JsonSyncMessage(SignalServiceSyncMessage syncMessage, Manager m) {
-        this.sentMessage = syncMessage.getSent().isPresent()
-                ? new JsonSyncDataMessage(syncMessage.getSent().get(), m)
-                : null;
+    static JsonSyncMessage from(SignalServiceSyncMessage syncMessage, Manager m) {
+        final var sentMessage = syncMessage.getSent().isPresent() ? JsonSyncDataMessage.from(syncMessage.getSent()
+                .get(), m) : null;
+        final List<String> blockedNumbers;
+        final List<String> blockedGroupIds;
         if (syncMessage.getBlockedList().isPresent()) {
             final var base64 = Base64.getEncoder();
-            this.blockedNumbers = syncMessage.getBlockedList()
+            blockedNumbers = syncMessage.getBlockedList()
                     .get()
                     .getAddresses()
                     .stream()
                     .map(Util::getLegacyIdentifier)
                     .collect(Collectors.toList());
-            this.blockedGroupIds = syncMessage.getBlockedList()
+            blockedGroupIds = syncMessage.getBlockedList()
                     .get()
                     .getGroupIds()
                     .stream()
                     .map(base64::encodeToString)
                     .collect(Collectors.toList());
         } else {
-            this.blockedNumbers = null;
-            this.blockedGroupIds = null;
-        }
-        if (syncMessage.getRead().isPresent()) {
-            this.readMessages = syncMessage.getRead()
-                    .get()
-                    .stream()
-                    .map(JsonSyncReadMessage::new)
-                    .collect(Collectors.toList());
-        } else {
-            this.readMessages = null;
+            blockedNumbers = null;
+            blockedGroupIds = null;
         }
 
+        final var readMessages = syncMessage.getRead().isPresent() ? syncMessage.getRead()
+                .get()
+                .stream()
+                .map(JsonSyncReadMessage::from)
+                .collect(Collectors.toList()) : null;
+
+        final JsonSyncMessageType type;
         if (syncMessage.getContacts().isPresent()) {
-            this.type = JsonSyncMessageType.CONTACTS_SYNC;
+            type = JsonSyncMessageType.CONTACTS_SYNC;
         } else if (syncMessage.getGroups().isPresent()) {
-            this.type = JsonSyncMessageType.GROUPS_SYNC;
+            type = JsonSyncMessageType.GROUPS_SYNC;
         } else if (syncMessage.getRequest().isPresent()) {
-            this.type = JsonSyncMessageType.REQUEST_SYNC;
+            type = JsonSyncMessageType.REQUEST_SYNC;
         } else {
-            this.type = null;
+            type = null;
         }
+        return new JsonSyncMessage(sentMessage, blockedNumbers, blockedGroupIds, readMessages, type);
     }
 
-    JsonSyncMessage(Signal.SyncMessageReceived messageReceived) {
-        this.sentMessage = new JsonSyncDataMessage(messageReceived);
-        this.blockedNumbers = null;
-        this.blockedGroupIds = null;
-        this.readMessages = null;
-        this.type = null;
+    static JsonSyncMessage from(Signal.SyncMessageReceived messageReceived) {
+        return new JsonSyncMessage(JsonSyncDataMessage.from(messageReceived), null, null, null, null);
     }
 }
index 042ed7e4e18b3851e1598bc8ff3069bdf39041e5..05cb48f3a2655841752abd1956a2459d22f3296a 100644 (file)
@@ -1,31 +1,19 @@
 package org.asamk.signal.json;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
 
 import static org.asamk.signal.util.Util.getLegacyIdentifier;
 
-class JsonSyncReadMessage {
-
-    @JsonProperty
-    @Deprecated
-    final String sender;
-
-    @JsonProperty
-    final String senderNumber;
-
-    @JsonProperty
-    final String senderUuid;
-
-    @JsonProperty
-    final long timestamp;
-
-    public JsonSyncReadMessage(final ReadMessage readMessage) {
-        final var sender = readMessage.getSender();
-        this.sender = getLegacyIdentifier(sender);
-        this.senderNumber = sender.getNumber().orNull();
-        this.senderUuid = sender.getUuid().toString();
-        this.timestamp = readMessage.getTimestamp();
+record JsonSyncReadMessage(
+        @Deprecated String sender, String senderNumber, String senderUuid, long timestamp
+) {
+
+    static JsonSyncReadMessage from(final ReadMessage readMessage) {
+        final var senderAddress = readMessage.getSender();
+        final var sender = getLegacyIdentifier(senderAddress);
+        final var senderNumber = senderAddress.getNumber().orNull();
+        final var senderUuid = senderAddress.getUuid().toString();
+        final var timestamp = readMessage.getTimestamp();
+        return new JsonSyncReadMessage(sender, senderNumber, senderUuid, timestamp);
     }
 }
index 7e3b1a44a966cec122ece3ad7e387ae961e39331..b231d67e457b1d51b36955f7eed76e11c22522f8 100644 (file)
@@ -1,28 +1,26 @@
 package org.asamk.signal.json;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 
 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
 
 import java.util.Base64;
 
-class JsonTypingMessage {
+record JsonTypingMessage(
+        String action, long timestamp, @JsonInclude(JsonInclude.Include.NON_NULL) String groupId
+) {
 
-    @JsonProperty
-    final String action;
-
-    @JsonProperty
-    final long timestamp;
-
-    @JsonProperty
-    @JsonInclude(JsonInclude.Include.NON_NULL)
-    final String groupId;
+    JsonTypingMessage(final String action, final long timestamp, final String groupId) {
+        this.action = action;
+        this.timestamp = timestamp;
+        this.groupId = groupId;
+    }
 
-    JsonTypingMessage(SignalServiceTypingMessage typingMessage) {
-        this.action = typingMessage.getAction().name();
-        this.timestamp = typingMessage.getTimestamp();
+    static JsonTypingMessage from(SignalServiceTypingMessage typingMessage) {
+        final var action = typingMessage.getAction().name();
+        final var timestamp = typingMessage.getTimestamp();
         final var encoder = Base64.getEncoder();
-        this.groupId = typingMessage.getGroupId().transform(encoder::encodeToString).orNull();
+        final var groupId = typingMessage.getGroupId().transform(encoder::encodeToString).orNull();
+        return new JsonTypingMessage(action, timestamp, groupId);
     }
 }
index d1b632127646d7387d7723cd1d81c6557880940f..1ffdb01623b50c2f6c6b416cd13fa36a1e0f13e8 100644 (file)
@@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 
 import java.util.List;
 
-public class JsonRpcBulkMessage extends JsonRpcMessage {
+public final class JsonRpcBulkMessage extends JsonRpcMessage {
 
     List<JsonNode> messages;
 
index 7f8b0a1a99553e1da8a60ce926dd8620c9eec69a..9cc7c87d2daea8c51897374a08986fd32a969071 100644 (file)
@@ -4,6 +4,6 @@ package org.asamk.signal.jsonrpc;
  * Represents a JSON-RPC (bulk) request or (bulk) response.
  * https://www.jsonrpc.org/specification
  */
-public abstract class JsonRpcMessage {
+public sealed abstract class JsonRpcMessage permits JsonRpcBulkMessage, JsonRpcRequest, JsonRpcResponse {
 
 }
index 1ae8552a8cb4af03f3382aa2d0d0f16c4c239608..ac54c7b7e774d3d2dbba2c491768b46fc232aaff 100644 (file)
@@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.node.ValueNode;
  * Represents a JSON-RPC request.
  * https://www.jsonrpc.org/specification#request_object
  */
-public class JsonRpcRequest extends JsonRpcMessage {
+public final class JsonRpcRequest extends JsonRpcMessage {
 
     /**
      * A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
index b5279b7d75501a9345d182282e3c407edf7d479e..406212d825ab6482df97c20c09571e2cd7b291b1 100644 (file)
@@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.node.ValueNode;
  * Represents a JSON-RPC response.
  * https://www.jsonrpc.org/specification#response_object
  */
-public class JsonRpcResponse extends JsonRpcMessage {
+public final class JsonRpcResponse extends JsonRpcMessage {
 
     /**
      * A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
index 8e824d34c70efde264eca0140b8569cb815adfd2..1c8a8f38fb2942286428dbc31675b4e12e1b3223 100644 (file)
@@ -64,12 +64,12 @@ public class ErrorUtils {
                     "CAPTCHA proof required for sending to \"%s\", available options \"%s\" with challenge token \"%s\", or wait \"%d\" seconds.\n"
                             + (
                             failure.getOptions().contains(ProofRequiredException.Option.RECAPTCHA)
-                                    ?
-                                    "To get the captcha token, go to https://signalcaptchas.org/registration/generate.html\n"
-                                            + "Check the developer tools (F12) console for a failed redirect to signalcaptcha://\n"
-                                            + "Everything after signalcaptcha:// is the captcha token.\n"
-                                            + "Use the following command to submit the captcha token:\n"
-                                            + "signal-cli submitRateLimitChallenge --challenge CHALLENGE_TOKEN --captcha CAPTCHA_TOKEN"
+                                    ? """
+                                    To get the captcha token, go to https://signalcaptchas.org/challenge/generate.html
+                                    Check the developer tools (F12) console for a failed redirect to signalcaptcha://
+                                    Everything after signalcaptcha:// is the captcha token.
+                                    Use the following command to submit the captcha token:
+                                    signal-cli submitRateLimitChallenge --challenge CHALLENGE_TOKEN --captcha CAPTCHA_TOKEN"""
                                     : ""
                     ),
                     identifier,