diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6c1f00b0..1924d640 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- java: [ '1.8', '12.0.2' ]
+ java: [ '11', '14' ]
steps:
- uses: actions/checkout@v1
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..edf8a5a2
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,60 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [master, ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [master]
+ schedule:
+ - cron: '0 7 * * 4'
+
+jobs:
+ analyse:
+ name: Analyse
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - name: Setup Java JDK
+ uses: actions/setup-java@v1.3.0
+ with:
+ java-version: 11
+
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ # We must fetch at least the immediate parents so that if this is
+ # a pull request then we can checkout the head.
+ fetch-depth: 2
+
+ # If this run was triggered by a pull request event, then checkout
+ # the head of the pull request instead of the merge commit.
+ - run: git checkout HEAD^2
+ if: ${{ github.event_name == 'pull_request' }}
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ # Override language selection by uncommenting this and choosing your languages
+ # with:
+ # languages: go, javascript, csharp, python, cpp, java
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 9f37145b..4953eaca 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -4,6 +4,28 @@
+
+
+
diff --git a/README.md b/README.md
index b4bed3b8..fb0d9d88 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ signal-cli is primarily intended to be used on servers to notify admins of impor
## Installation
-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/). You need to have at least JRE 7 installed, to run signal-cli.
+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. You need to have at least JRE 8 installed, to run signal-cli.
### Install system-wide on Linux
See [latest version](https://github.com/AsamK/signal-cli/releases).
@@ -18,12 +18,12 @@ sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
```
You can find further instructions on the Wiki:
-- [Install on Ubuntu](https://github.com/AsamK/signal-cli/wiki/HowToUbuntu)
+- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
- [DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service)
## Usage
-Important: The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes.
+Important: The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes.)
* Register a number (with SMS verification)
@@ -31,7 +31,7 @@ Important: The USERNAME (your phone number) must include the country calling cod
You can register Signal using a land line number. In this case you can skip SMS verification process and jump directly to the voice call verification by adding the --voice switch at the end of above register command.
-* Verify the number using the code received via SMS or voice
+* Verify the number using the code received via SMS or voice, optionally add `--pin PIN_CODE` if you've added a pin code to your account
signal-cli -u USERNAME verify CODE
diff --git a/build.gradle b/build.gradle
index a7312bb4..0607c12b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,28 +2,26 @@ apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'eclipse'
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
+sourceCompatibility = JavaVersion.VERSION_11
+targetCompatibility = JavaVersion.VERSION_11
mainClassName = 'org.asamk.signal.Main'
-version = '0.6.5'
+version = '0.6.10'
compileJava.options.encoding = 'UTF-8'
repositories {
mavenLocal()
- maven {
- url "https://raw.github.com/AsamK/maven/master/releases/"
- }
mavenCentral()
}
dependencies {
- compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_1'
- compile 'org.bouncycastle:bcprov-jdk15on:1.64'
- compile 'net.sourceforge.argparse4j:argparse4j:0.8.1'
- compile 'org.freedesktop.dbus:dbus-java:2.7.0'
+ implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_14'
+ implementation 'org.bouncycastle:bcprov-jdk15on:1.66'
+ implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1'
+ implementation 'com.github.hypfvieh:dbus-java:3.2.3'
+ implementation 'org.slf4j:slf4j-nop:1.7.30'
}
jar {
@@ -46,7 +44,6 @@ run {
// Find any 3rd party libraries which have released new versions
// to the central Maven repo since we last upgraded.
-// http://daniel.gredler.net/2011/08/08/gradle-keeping-libraries-up-to-date/
task checkLibVersions {
doLast {
def checked = [:]
@@ -54,20 +51,16 @@ task checkLibVersions {
configurations.each { configuration ->
configuration.allDependencies.each { dependency ->
def version = dependency.version
- if (!version.contains('SNAPSHOT') && !checked[dependency]) {
+ if (!checked[dependency]) {
def group = dependency.group
def path = group.replace('.', '/')
def name = dependency.name
def url = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
try {
def metadata = new XmlSlurper().parseText(url.toURL().text)
- def versions = metadata.versioning.versions.version.collect { it.text() }
- versions.removeAll { it.toLowerCase().contains('alpha') }
- versions.removeAll { it.toLowerCase().contains('beta') }
- versions.removeAll { it.toLowerCase().contains('rc') }
- def newest = versions.max()
- if (version != newest) {
- println "$group:$name $version -> $newest"
+ def newest = metadata.versioning.latest;
+ if ("$version" != "$newest") {
+ println "UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}"
}
} catch (FileNotFoundException e) {
logger.debug "Unable to download $url: $e.message"
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index f3d88b1c..e708b1c0 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1b16c34a..12d38de6 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 2fe81a7d..4f906e0c 100755
--- a/gradlew
+++ b/gradlew
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
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
diff --git a/gradlew.bat b/gradlew.bat
index 24467a14..ac1b06f9 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -51,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -61,28 +64,14 @@ echo location of your Java installation.
goto fail
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc
index 35a578f4..98a5da2a 100644
--- a/man/signal-cli.1.adoc
+++ b/man/signal-cli.1.adoc
@@ -5,254 +5,272 @@ vim:set ts=4 sw=4 tw=82 noet:
= signal-cli (1)
-Name
-----
+== Name
+
signal-cli - A commandline and dbus interface for the Signal messenger
-Synopsis
---------
+== Synopsis
+
*signal-cli* [--config CONFIG] [-h | -v | -u USERNAME | --dbus | --dbus-system] command [command-options]
-Description
------------
+== Description
-signal-cli is a commandline interface for libsignal-service-java. It supports
-registering, verifying, sending and receiving messages. For registering you need a
-phone number where you can receive SMS or incoming calls.
-signal-cli was primarily developed to be used on servers to notify admins of
-important events. For this use-case, it has a dbus interface, that can be used to
-send messages from any programming language that has dbus bindings.
+signal-cli is a commandline interface for libsignal-service-java.
+It supports registering, verifying, sending and receiving messages.
+For registering you need a phone number where you can receive SMS or incoming calls.
+signal-cli was primarily developed to be used on servers to notify admins of important events.
+For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings.
-Options
--------
+== Options
*-h*, *--help*::
- Show help message and quit.
+Show help message and quit.
*-v*, *--version*::
- Print the version and quit.
+Print the version and quit.
*--config* CONFIG::
- Set the path, where to store the config.
- Make sure you have full read/write access to the given directory.
- (Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`))
+Set the path, where to store the config.
+Make sure you have full read/write access to the given directory.
+(Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`))
*-u* USERNAME, *--username* USERNAME::
- Specify your phone number, that will be your identifier.
- The phone number must include the country calling code, i.e. the number must
- start with a "+" sign.
+Specify your phone number, that will be your identifier.
+The phone number must include the country calling code, i.e. the number must start with a "+" sign.
*--dbus*::
- Make request via user dbus.
+Make request via user dbus.
*--dbus-system*::
- Make request via system dbus.
+Make request via system dbus.
-Commands
---------
+== Commands
-register
-~~~~~~~~
-Register a phone number with SMS or voice verification. Use the verify command to
-complete the verification.
+=== register
+
+Register a phone number with SMS or voice verification.
+Use the verify command to complete the verification.
*-v*, *--voice*::
- The verification should be done over voice, not SMS.
+The verification should be done over voice, not SMS.
+
+=== verify
-verify
-~~~~~~
Verify the number using the code received via SMS or voice.
VERIFICATIONCODE::
- The verification code.
+The verification code.
*-p* PIN, *--pin* PIN::
- The registration lock PIN, that was set by the user. Only required if a PIN was set.
+The registration lock PIN, that was set by the user.
+Only required if a PIN was set.
+
+=== unregister
-unregister
-~~~~~~~~~~
Disable push support for this device, i.e. this device won't receive any more messages.
If this is the master device, other users can't send messages to this number anymore.
Use "updateAccount" to undo this.
To remove a linked device, use "removeDevice" from the master device.
-updateAccount
-~~~~~~~~~~~~~
+=== updateAccount
+
Update the account attributes on the signal server.
Can fix problems with receiving messages.
-setPin
-~~~~~~
+=== setPin
+
Set a registration lock pin, to prevent others from registering this number.
REGISTRATION_LOCK_PIN::
- The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity)
+The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity)
+
+=== removePin
-removePin
-~~~~~~~~~
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. 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.
+=== 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. 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::
- Optionally specify a name to describe this new device. By default "cli" will
- be used.
+Optionally specify a name to describe this new device.
+By default "cli" will be used.
-addDevice
-~~~~~~~~~
-Link another device to this device. Only works, if this is the master device.
+=== addDevice
+
+Link another device to this device.
+Only works, if this is the master device.
*--uri* URI::
- Specify the uri contained in the QR code shown by the new device.
+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=....."
+
+=== listDevices
-listDevices
-~~~~~~~~~~~
Show a list of connected devices.
-removeDevice
-~~~~~~~~~~~~
-Remove a connected device. Only works, if this is the master device.
+=== removeDevice
+
+Remove a connected device.
+Only works, if this is the master device.
*-d* DEVICEID, *--deviceId* DEVICEID::
- Specify the device you want to remove. Use listDevices to see the deviceIds.
+Specify the device you want to remove.
+Use listDevices to see the deviceIds.
+
+=== send
-send
-~~~~
Send a message to another user or group.
RECIPIENT::
- Specify the recipients’ phone number.
+Specify the recipients’ phone number.
*-g* GROUP, *--group* GROUP::
- Specify the recipient group ID in base64 encoding.
+Specify the recipient group ID in base64 encoding.
*-m* MESSAGE, *--message* MESSAGE::
- Specify the message, if missing, standard input is used.
+Specify the message, if missing, standard input is used.
*-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]::
- Add one or more files as attachment.
+Add one or more files as attachment.
*-e*, *--endsession*::
- Clear session state and send end session message.
+Clear session state and send end session message.
-receive
-~~~~~~~
-Query the server for new messages. New messages are printed on standardoutput and
-attachments are downloaded to the config directory.
+=== sendReaction
+
+Send reaction to a previously received or sent message.
+
+RECIPIENT::
+Specify the recipients’ phone number.
+
+*-g* GROUP, *--group* GROUP::
+Specify the recipient group ID in base64 encoding.
+
+*-e* EMOJI, *--emoji* EMOJI::
+Specify the emoji, should be a single unicode grapheme cluster.
+
+*-a* NUMBER, *--target-author* NUMBER::
+Specify the number of the author of the message to which to react.
+
+*-t* TIMESTAMP, *--target-timestamp* TIMESTAMP::
+Specify the timestamp of the message to which to react.
+
+*-r*, *--remove*::
+Remove a reaction.
+
+=== receive
+
+Query the server for new messages.
+New messages are printed on standardoutput and attachments are downloaded to the config directory.
*-t* TIMEOUT, *--timeout* TIMEOUT::
- Number of seconds to wait for new messages (negative values disable timeout).
- Default is 5 seconds.
+Number of seconds to wait for new messages (negative values disable timeout).
+Default is 5 seconds.
*--ignore-attachments*::
- Don’t download attachments of received messages.
+Don’t download attachments of received messages.
*--json*::
- Output received messages in json format, one object per line.
+Output received messages in json format, one object per line.
+
+=== updateGroup
-updateGroup
-~~~~~~~~~~~
Create or update a group.
*-g* GROUP, *--group* GROUP::
- Specify the recipient group ID in base64 encoding. If not specified, a new
- group with a new random ID is generated.
+Specify the recipient group ID in base64 encoding.
+If not specified, a new group with a new random ID is generated.
*-n* NAME, *--name* NAME::
- Specify the new group name.
+Specify the new group name.
*-a* AVATAR, *--avatar* AVATAR::
- Specify a new group avatar image file.
+Specify a new group avatar image file.
*-m* [MEMBER [MEMBER ...]], *--member* [MEMBER [MEMBER ...]]::
- Specify one or more members to add to the group.
+Specify one or more members to add to the group.
+
+=== quitGroup
-quitGroup
-~~~~~~~~~
Send a quit group message to all group members and remove self from member list.
*-g* GROUP, *--group* GROUP::
- Specify the recipient group ID in base64 encoding.
+Specify the recipient group ID in base64 encoding.
+
+=== listGroups
-listGroups
-~~~~~~~~~~~
Show a list of known groups.
*-d*, *--detailed*::
- Include the list of members of each group.
+Include the list of members of each group.
-listIdentities
-~~~~~~~~~~~~~~
-List all known identity keys and their trust status, fingerprint and safety
-number.
+=== listIdentities
+
+List all known identity keys and their trust status, fingerprint and safety number.
*-n* NUMBER, *--number* NUMBER::
- Only show identity keys for the given phone number.
+Only show identity keys for the given phone number.
-trust
-~~~~~
-Set the trust level of a given number. The first time a key for a number is seen,
-it is trusted by default (TOFU). If the key changes, the new key must be trusted
-manually.
+=== trust
+
+Set the trust level of a given number.
+The first time a key for a number is seen, it is trusted by default (TOFU).
+If the key changes, the new key must be trusted manually.
number::
- Specify the phone number, for which to set the trust.
+Specify the phone number, for which to set the trust.
*-a*, *--trust-all-known-keys*::
- Trust all known keys of this user, only use this for testing.
+Trust all known keys of this user, only use this for testing.
-*-v* VERIFIED_FINGERPRINT, *--verified-fingerprint* VERIFIED_FINGERPRINT::
- Specify the safety number or fingerprint of the key, only use this option if you have verified
- the fingerprint.
+*-v* VERIFIED_SAFETY_NUMBER, *--verified-safety-number* VERIFIED_SAFETY_NUMBER::
+Specify the safety number of the key, only use this option if you have verified the safety number.
-updateProfile
-~~~~~~~~~~~~~
-Update the name and/or avatar image visible by message recipients for the current users.
-The profile is stored encrypted on the Signal servers. The decryption key is sent
-with every outgoing messages (excluding group messages).
+=== updateProfile
+
+Update the name and avatar image visible by message recipients for the current users.
+The profile is stored encrypted on the Signal servers.
+The decryption key is sent with every outgoing messages (excluding group messages).
*--name*::
- New name visible by message recipients.
+New name visible by message recipients.
*--avatar*::
- Path to the new avatar visible by message recipients.
+Path to the new avatar visible by message recipients.
*--remove-avatar*::
- Remove the avatar visible by message recipients.
+Remove the avatar visible by message recipients.
-updateContact
-~~~~~~~~~~~~~
-Update the info associated to a number on our contact list. This change is only
-local but can be synchronized to other devices by using `sendContacts` (see
-below).
+=== updateContact
+
+Update the info associated to a number on our contact list.
+This change is only local but can be synchronized to other devices by using `sendContacts` (see below).
If the contact doesn't exist yet, it will be added.
NUMBER::
- Specify the contact phone number.
+Specify the contact phone number.
*-n*, *--name*::
- Specify the new name for this contact.
+Specify the new name for this contact.
-block
-~~~~~
-Block the given contacts or groups (no messages will be received). This change is only
-local but can be synchronized to other devices by using `sendContacts` (see
-below).
+*-e*, *--expiration*::
+Set expiration time of messages (seconds).
+To disable expiration set expiration time to 0.
+
+=== block
+
+Block the given contacts or groups (no messages will be received).
+This change is only local but can be synchronized to other devices by using `sendContacts` (see below).
[CONTACT [CONTACT ...]]::
- Specify the phone numbers of contacts that should be blocked.
+Specify the phone numbers of contacts that should be blocked.
*-g* [GROUP [GROUP ...]], *--group* [GROUP [GROUP ...]]::
- Specify the group IDs that should be blocked in base64 encoding.
+Specify the group IDs that should be blocked in base64 encoding.
-unblock
-~~~~~~~
-Unblock the given contacts or groups (messages will be received again). This change is only
-local but can be synchronized to other devices by using `sendContacts` (see
-below).
+=== unblock
+
+Unblock the given contacts or groups (messages will be received again).
+This change is only local but can be synchronized to other devices by using `sendContacts` (see below).
[CONTACT [CONTACT ...]]::
Specify the phone numbers of contacts that should be unblocked.
@@ -260,60 +278,82 @@ Specify the phone numbers of contacts that should be unblocked.
*-g* [GROUP [GROUP ...]], *--group* [GROUP [GROUP ...]]::
Specify the group IDs that should be unblocked in base64 encoding.
-sendContacts
-~~~~~~~~~~~~
+=== sendContacts
+
Send a synchronization message with the local contacts list to all linked devices.
This command should only be used if this is the master device.
-daemon
-~~~~~~
-signal-cli can run in daemon mode and provides an experimental dbus interface. For
-dbus support you need jni/unix-java.so installed on your system (Debian:
-libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)).
+=== uploadStickerPack
+
+Upload a new sticker pack, consisting of a manifest file and the stickers in WebP format (maximum size for a sticker file is 100KiB).
+The required manifest.json has the following format:
+
+[source,json]
+----
+{
+ "title": "",
+ "author": "",
+ "cover": { // Optional cover, by default the first sticker is used as cover
+ "file": "",
+ "emoji": ""
+ },
+ "stickers": [
+ {
+ "file": "",
+ "emoji": ""
+ }
+ ...
+ ]
+}
+----
+
+PATH::
+The path of the manifest.json or a zip file containing the sticker pack you wish to upload.
+
+=== daemon
+
+signal-cli can run in daemon mode and provides an experimental dbus interface.
*--system*::
- Use DBus system bus instead of user bus.
+Use DBus system bus instead of user bus.
*--ignore-attachments*::
- Don’t download attachments of received messages.
+Don’t download attachments of received messages.
-
-Examples
---------
+== Examples
Register a number (with SMS verification)::
- signal-cli -u USERNAME register
+signal-cli -u USERNAME register
Verify the number using the code received via SMS or voice::
- signal-cli -u USERNAME verify CODE
+signal-cli -u USERNAME verify CODE
Send a message to one or more recipients::
- signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]]
+signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]]
Pipe the message content from another process::
- uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]]
+uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]]
Create a group::
- signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]]
+signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]]
Add member to a group::
- signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER"
+signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER"
Leave a group::
- signal-cli -u USERNAME quitGroup -g GROUP_ID
+signal-cli -u USERNAME quitGroup -g GROUP_ID
Send a message to a group::
- signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID
+signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID
Trust new key, after having verified it::
- signal-cli -u USERNAME trust -v FINGER_PRINT NUMBER
+signal-cli -u USERNAME trust -v SAFETY_NUMBER NUMBER
Trust new key, without having verified it. Only use this if you don't care about security::
- signal-cli -u USERNAME trust -a NUMBER
+signal-cli -u USERNAME trust -a NUMBER
-Files
------
-The password and cryptographic keys are created when registering and stored in the
-current users home directory, the directory can be changed with *--config*:
+== Files
+
+The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*:
`$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`)
@@ -323,10 +363,8 @@ For legacy users, the old config directories are used as a fallback:
$HOME/.config/textsecure/
+== Authors
-Authors
--------
-
-Maintained by AsamK , who is assisted by other open
-source contributors. For more information about signal-cli development, see
+Maintained by AsamK , who is assisted by other open source contributors.
+For more information about signal-cli development, see
.
diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java
index 8c9c525f..a93d6d86 100644
--- a/src/main/java/org/asamk/Signal.java
+++ b/src/main/java/org/asamk/Signal.java
@@ -1,33 +1,33 @@
package org.asamk;
-import org.asamk.signal.AttachmentInvalidException;
-import org.asamk.signal.GroupNotFoundException;
-import org.freedesktop.dbus.DBusInterface;
-import org.freedesktop.dbus.DBusSignal;
import org.freedesktop.dbus.exceptions.DBusException;
-import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
-import org.whispersystems.signalservice.api.util.InvalidNumberException;
+import org.freedesktop.dbus.exceptions.DBusExecutionException;
+import org.freedesktop.dbus.interfaces.DBusInterface;
+import org.freedesktop.dbus.messages.DBusSignal;
-import java.io.IOException;
import java.util.List;
+/**
+ * DBus interface for the org.asamk.Signal service.
+ * Including emitted Signals and returned Errors.
+ */
public interface Signal extends DBusInterface {
- void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
+ long sendMessage(String message, List attachments, String recipient) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
- void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
+ long sendMessage(String message, List attachments, List recipients) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
- void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions;
+ void sendEndSessionMessage(List recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
- void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException;
+ long sendGroupMessage(String message, List attachments, byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.UnregisteredUser, Error.UntrustedIdentity;
- String getContactName(String number) throws InvalidNumberException;
+ String getContactName(String number) throws Error.InvalidNumber;
- void setContactName(String number, String name) throws InvalidNumberException;
+ void setContactName(String number, String name) throws Error.InvalidNumber;
- void setContactBlocked(String number, boolean blocked) throws InvalidNumberException;
+ void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber;
- void setGroupBlocked(byte[] groupId, boolean blocked) throws GroupNotFoundException;
+ void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound;
List getGroupIds();
@@ -35,17 +35,17 @@ public interface Signal extends DBusInterface {
List getGroupMembers(byte[] groupId);
- byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException;
+ byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity;
boolean isRegistered();
class MessageReceived extends DBusSignal {
- private long timestamp;
- private String sender;
- private byte[] groupId;
- private String message;
- private List attachments;
+ private final long timestamp;
+ private final String sender;
+ private final byte[] groupId;
+ private final String message;
+ private final List attachments;
public MessageReceived(String objectpath, long timestamp, String sender, byte[] groupId, String message, List attachments) throws DBusException {
super(objectpath, timestamp, sender, groupId, message, attachments);
@@ -79,8 +79,8 @@ public interface Signal extends DBusInterface {
class ReceiptReceived extends DBusSignal {
- private long timestamp;
- private String sender;
+ private final long timestamp;
+ private final String sender;
public ReceiptReceived(String objectpath, long timestamp, String sender) throws DBusException {
super(objectpath, timestamp, sender);
@@ -96,4 +96,93 @@ public interface Signal extends DBusInterface {
return sender;
}
}
+
+ class SyncMessageReceived extends DBusSignal {
+
+ private final long timestamp;
+ private final String source;
+ private final String destination;
+ private final byte[] groupId;
+ private final String message;
+ private final List attachments;
+
+ public SyncMessageReceived(String objectpath, long timestamp, String source, String destination, byte[] groupId, String message, List attachments) throws DBusException {
+ super(objectpath, timestamp, source, destination, groupId, message, attachments);
+ this.timestamp = timestamp;
+ this.source = source;
+ this.destination = destination;
+ this.groupId = groupId;
+ this.message = message;
+ this.attachments = attachments;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public byte[] getGroupId() {
+ return groupId;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public List getAttachments() {
+ return attachments;
+ }
+ }
+
+ interface Error {
+
+ class AttachmentInvalid extends DBusExecutionException {
+
+ public AttachmentInvalid(final String message) {
+ super(message);
+ }
+ }
+
+ class Failure extends DBusExecutionException {
+
+ public Failure(final String message) {
+ super(message);
+ }
+ }
+
+ class GroupNotFound extends DBusExecutionException {
+
+ public GroupNotFound(final String message) {
+ super(message);
+ }
+ }
+
+ class InvalidNumber extends DBusExecutionException {
+
+ public InvalidNumber(final String message) {
+ super(message);
+ }
+ }
+
+ class UnregisteredUser extends DBusExecutionException {
+
+ public UnregisteredUser(final String message) {
+ super(message);
+ }
+ }
+
+ class UntrustedIdentity extends DBusExecutionException {
+
+ public UntrustedIdentity(final String message) {
+ super(message);
+ }
+ }
+ }
}
diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java
new file mode 100644
index 00000000..afafc7d7
--- /dev/null
+++ b/src/main/java/org/asamk/signal/BaseConfig.java
@@ -0,0 +1,12 @@
+package org.asamk.signal;
+
+public class BaseConfig {
+
+ public final static String PROJECT_NAME = BaseConfig.class.getPackage().getImplementationTitle();
+ public final static String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion();
+
+ final static String USER_AGENT = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + " " + PROJECT_VERSION;
+
+ private BaseConfig() {
+ }
+}
diff --git a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java
index cebabc18..8fb11a59 100644
--- a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java
@@ -1,7 +1,7 @@
package org.asamk.signal;
import org.asamk.signal.manager.Manager;
-import org.freedesktop.dbus.DBusConnection;
+import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
diff --git a/src/main/java/org/asamk/signal/JsonDataMessage.java b/src/main/java/org/asamk/signal/JsonDataMessage.java
deleted file mode 100644
index 34f6249e..00000000
--- a/src/main/java/org/asamk/signal/JsonDataMessage.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.asamk.signal;
-
-import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
-import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
-
-import java.util.ArrayList;
-import java.util.List;
-
-class JsonDataMessage {
-
- long timestamp;
- String message;
- int expiresInSeconds;
- List attachments;
- JsonGroupInfo groupInfo;
-
- JsonDataMessage(SignalServiceDataMessage dataMessage) {
- this.timestamp = dataMessage.getTimestamp();
- if (dataMessage.getGroupInfo().isPresent()) {
- this.groupInfo = new JsonGroupInfo(dataMessage.getGroupInfo().get());
- }
- if (dataMessage.getBody().isPresent()) {
- this.message = dataMessage.getBody().get();
- }
- this.expiresInSeconds = dataMessage.getExpiresInSeconds();
- if (dataMessage.getAttachments().isPresent()) {
- this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size());
- for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) {
- this.attachments.add(new JsonAttachment(attachment));
- }
- } else {
- this.attachments = new ArrayList<>();
- }
- }
-}
diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java
index 6b26ea0e..0728b871 100644
--- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java
@@ -2,13 +2,16 @@ package org.asamk.signal;
import org.asamk.Signal;
import org.asamk.signal.manager.Manager;
-import org.freedesktop.dbus.DBusConnection;
+import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import java.util.ArrayList;
import java.util.List;
@@ -28,7 +31,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, final String objectPath, Manager m) {
if (envelope.isReceipt()) {
try {
- conn.sendSignal(new Signal.ReceiptReceived(
+ conn.sendMessage(new Signal.ReceiptReceived(
objectPath,
envelope.getTimestamp(),
!envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get()
@@ -36,36 +39,81 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
} catch (DBusException e) {
e.printStackTrace();
}
- } else if (content != null && content.getDataMessage().isPresent()) {
- SignalServiceDataMessage message = content.getDataMessage().get();
-
- if (!message.isEndSession() &&
- !(message.getGroupInfo().isPresent() &&
- message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) {
- List attachments = new ArrayList<>();
- if (message.getAttachments().isPresent()) {
- for (SignalServiceAttachment attachment : message.getAttachments().get()) {
- if (attachment.isPointer()) {
- attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath());
+ } else if (content != null) {
+ if (content.getReceiptMessage().isPresent()) {
+ final SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get();
+ if (receiptMessage.isDeliveryReceipt()) {
+ final String sender = !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get();
+ for (long timestamp : receiptMessage.getTimestamps()) {
+ try {
+ conn.sendMessage(new Signal.ReceiptReceived(
+ objectPath,
+ timestamp,
+ sender
+ ));
+ } catch (DBusException e) {
+ e.printStackTrace();
}
}
}
+ } else if (content.getDataMessage().isPresent()) {
+ SignalServiceDataMessage message = content.getDataMessage().get();
- try {
- conn.sendSignal(new Signal.MessageReceived(
- objectPath,
- message.getTimestamp(),
- envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(),
- message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0],
- message.getBody().isPresent() ? message.getBody().get() : "",
- attachments));
- } catch (DBusException e) {
- e.printStackTrace();
+ if (!message.isEndSession() &&
+ !(message.getGroupContext().isPresent() &&
+ message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) {
+ try {
+ conn.sendMessage(new Signal.MessageReceived(
+ objectPath,
+ message.getTimestamp(),
+ envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(),
+ message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
+ ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
+ message.getBody().isPresent() ? message.getBody().get() : "",
+ JsonDbusReceiveMessageHandler.getAttachments(message, m)));
+ } catch (DBusException e) {
+ e.printStackTrace();
+ }
+ }
+ } else if (content.getSyncMessage().isPresent()) {
+ SignalServiceSyncMessage sync_message = content.getSyncMessage().get();
+ if (sync_message.getSent().isPresent()) {
+ SentTranscriptMessage transcript = sync_message.getSent().get();
+
+ if (!envelope.isUnidentifiedSender() && envelope.hasSource() && (transcript.getDestination().isPresent() || transcript.getMessage().getGroupContext().isPresent())) {
+ SignalServiceDataMessage message = transcript.getMessage();
+
+ try {
+ conn.sendMessage(new Signal.SyncMessageReceived(
+ objectPath,
+ transcript.getTimestamp(),
+ envelope.getSourceAddress().getNumber().get(),
+ transcript.getDestination().isPresent() ? transcript.getDestination().get().getNumber().get() : "",
+ message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
+ ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
+ message.getBody().isPresent() ? message.getBody().get() : "",
+ JsonDbusReceiveMessageHandler.getAttachments(message, m)));
+ } catch (DBusException e) {
+ e.printStackTrace();
+ }
+ }
}
}
}
}
+ static private List getAttachments(SignalServiceDataMessage message, Manager m) {
+ List attachments = new ArrayList<>();
+ if (message.getAttachments().isPresent()) {
+ for (SignalServiceAttachment attachment : message.getAttachments().get()) {
+ if (attachment.isPointer()) {
+ attachments.add(m.getAttachmentFile(attachment.asPointer().getRemoteId()).getAbsolutePath());
+ }
+ }
+ }
+ return attachments;
+ }
+
@Override
public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) {
super.handleMessage(envelope, content, exception);
diff --git a/src/main/java/org/asamk/signal/JsonError.java b/src/main/java/org/asamk/signal/JsonError.java
deleted file mode 100644
index 5ef2cd7c..00000000
--- a/src/main/java/org/asamk/signal/JsonError.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.asamk.signal;
-
-class JsonError {
-
- String message;
-
- JsonError(Throwable exception) {
- this.message = exception.getMessage();
- }
-}
diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java
index 1aea2327..dfe51fe7 100644
--- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java
@@ -5,9 +5,10 @@ import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.asamk.signal.json.JsonError;
+import org.asamk.signal.json.JsonMessageEnvelope;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
@@ -23,7 +24,6 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
this.m = m;
this.jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
- jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
diff --git a/src/main/java/org/asamk/signal/JsonSyncMessage.java b/src/main/java/org/asamk/signal/JsonSyncMessage.java
deleted file mode 100644
index a6ecb459..00000000
--- a/src/main/java/org/asamk/signal/JsonSyncMessage.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.asamk.signal;
-
-import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
-import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-
-
-import java.util.ArrayList;
-import java.util.List;
-
-enum JsonSyncMessageType {
- CONTACTS_SYNC,
- GROUPS_SYNC,
- REQUEST_SYNC
-}
-
-class JsonSyncMessage {
-
- JsonSyncDataMessage sentMessage;
- List blockedNumbers;
- List readMessages;
- JsonSyncMessageType type;
-
- JsonSyncMessage(SignalServiceSyncMessage syncMessage) {
- if (syncMessage.getSent().isPresent()) {
- this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get());
- }
- if (syncMessage.getBlockedList().isPresent()) {
- this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size());
- for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) {
- this.blockedNumbers.add(address.getNumber().get());
- }
- }
- if (syncMessage.getRead().isPresent()) {
- this.readMessages = syncMessage.getRead().get();
- }
-
- if (syncMessage.getContacts().isPresent()) {
- this.type = JsonSyncMessageType.CONTACTS_SYNC;
- } else if (syncMessage.getGroups().isPresent()) {
- this.type = JsonSyncMessageType.GROUPS_SYNC;
- } else if (syncMessage.getRequest().isPresent()) {
- this.type = JsonSyncMessageType.REQUEST_SYNC;
- }
- }
-}
diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java
index 6ad3aee2..0e42e20d 100644
--- a/src/main/java/org/asamk/signal/Main.java
+++ b/src/main/java/org/asamk/signal/Main.java
@@ -31,16 +31,22 @@ import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
-import org.asamk.signal.manager.BaseConfig;
+import org.asamk.signal.commands.ProvisioningCommand;
+import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.ProvisioningManager;
+import org.asamk.signal.manager.ServiceConfig;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.freedesktop.dbus.DBusConnection;
+import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
+import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
+import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import java.io.File;
+import java.io.IOException;
import java.security.Security;
import java.util.Map;
@@ -68,82 +74,124 @@ public class Main {
private static int handleCommands(Namespace ns) {
final String username = ns.getString("username");
- Manager m;
- Signal ts;
- DBusConnection dBusConn = null;
- try {
- if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
- try {
- m = null;
- int busType;
- if (ns.getBoolean("dbus_system")) {
- busType = DBusConnection.SYSTEM;
- } else {
- busType = DBusConnection.SESSION;
- }
- dBusConn = DBusConnection.getConnection(busType);
- ts = dBusConn.getRemoteObject(
+
+ if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
+ try {
+ DBusConnection.DBusBusType busType;
+ if (ns.getBoolean("dbus_system")) {
+ busType = DBusConnection.DBusBusType.SYSTEM;
+ } else {
+ busType = DBusConnection.DBusBusType.SESSION;
+ }
+ try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
+ Signal ts = dBusConn.getRemoteObject(
DbusConfig.SIGNAL_BUSNAME, DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
- } catch (UnsatisfiedLinkError e) {
- System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
- return 1;
- } catch (DBusException e) {
- e.printStackTrace();
- if (dBusConn != null) {
- dBusConn.disconnect();
- }
- return 3;
- }
- } else {
- String dataPath = ns.getString("config");
- if (isEmpty(dataPath)) {
- dataPath = getDefaultDataPath();
- }
- m = new Manager(username, dataPath);
- ts = m;
+ return handleCommands(ns, ts, dBusConn);
+ }
+ } catch (UnsatisfiedLinkError e) {
+ System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
+ return 1;
+ } catch (DBusException | IOException e) {
+ e.printStackTrace();
+ return 3;
+ }
+ } else {
+ String dataPath = ns.getString("config");
+ if (isEmpty(dataPath)) {
+ dataPath = getDefaultDataPath();
+ }
+
+ final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT);
+
+ if (username == null) {
+ ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
+ return handleCommands(ns, pm);
+ }
+
+ Manager manager;
+ try {
+ manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
+ } catch (Throwable e) {
+ System.err.println("Error loading state file: " + e.getMessage());
+ return 2;
+ }
+
+ try (Manager m = manager) {
try {
- m.init();
- } catch (Exception e) {
- System.err.println("Error loading state file: " + e.getMessage());
+ m.checkAccountState();
+ } catch (AuthorizationFailedException e) {
+ if (!"register".equals(ns.getString("command"))) {
+ // Register command should still be possible, if current authorization fails
+ System.err.println("Authorization failed, was the number registered elsewhere?");
+ return 2;
+ }
+ } catch (IOException e) {
+ System.err.println("Error while checking account: " + e.getMessage());
return 2;
}
- }
- String commandKey = ns.getString("command");
- final Map commands = Commands.getCommands();
- if (commands.containsKey(commandKey)) {
- Command command = commands.get(commandKey);
-
- if (dBusConn != null) {
- if (command instanceof ExtendedDbusCommand) {
- return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
- } else if (command instanceof DbusCommand) {
- return ((DbusCommand) command).handleCommand(ns, ts);
- } else {
- System.err.println(commandKey + " is not yet implemented via dbus");
- return 1;
- }
- } else {
- if (command instanceof LocalCommand) {
- return ((LocalCommand) command).handleCommand(ns, m);
- } else if (command instanceof DbusCommand) {
- return ((DbusCommand) command).handleCommand(ns, ts);
- } else {
- System.err.println(commandKey + " is only works via dbus");
- return 1;
- }
- }
- }
- return 0;
- } finally {
- if (dBusConn != null) {
- dBusConn.disconnect();
+ return handleCommands(ns, m);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return 3;
}
}
}
+ private static int handleCommands(Namespace ns, Signal ts, DBusConnection dBusConn) {
+ String commandKey = ns.getString("command");
+ final Map commands = Commands.getCommands();
+ if (commands.containsKey(commandKey)) {
+ Command command = commands.get(commandKey);
+
+ if (command instanceof ExtendedDbusCommand) {
+ return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
+ } else if (command instanceof DbusCommand) {
+ return ((DbusCommand) command).handleCommand(ns, ts);
+ } else {
+ System.err.println(commandKey + " is not yet implemented via dbus");
+ return 1;
+ }
+ }
+ return 0;
+ }
+
+ private static int handleCommands(Namespace ns, ProvisioningManager pm) {
+ String commandKey = ns.getString("command");
+ final Map commands = Commands.getCommands();
+ if (commands.containsKey(commandKey)) {
+ Command command = commands.get(commandKey);
+
+ if (command instanceof ProvisioningCommand) {
+ return ((ProvisioningCommand) command).handleCommand(ns, pm);
+ } else {
+ System.err.println(commandKey + " only works with a username");
+ return 1;
+ }
+ }
+ return 0;
+ }
+
+ private static int handleCommands(Namespace ns, Manager m) {
+ String commandKey = ns.getString("command");
+ final Map commands = Commands.getCommands();
+ if (commands.containsKey(commandKey)) {
+ Command command = commands.get(commandKey);
+
+ if (command instanceof LocalCommand) {
+ return ((LocalCommand) command).handleCommand(ns, m);
+ } else if (command instanceof DbusCommand) {
+ return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m));
+ } else if (command instanceof ExtendedDbusCommand) {
+ System.err.println(commandKey + " only works via dbus");
+ }
+ return 1;
+ }
+ return 0;
+ }
+
/**
* Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist:
* - $HOME/.config/signal
diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
index 4cb09440..9a75aa1c 100644
--- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
@@ -76,7 +76,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (content == null) {
System.out.println("Failed to decrypt message.");
} else {
- System.out.println(String.format("Sender: %s (device: %d)", content.getSender().getNumber().get(), content.getSenderDevice()));
+ ContactInfo sourceContact = m.getContact(content.getSender().getNumber().get());
+ System.out.println(String.format("Sender: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + content.getSender().getNumber().get(), content.getSenderDevice()));
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
handleSignalServiceDataMessage(message);
@@ -102,7 +103,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Received sync read messages list");
for (ReadMessage rm : syncMessage.getRead().get()) {
ContactInfo fromContact = m.getContact(rm.getSender().getNumber().get());
- System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender().getNumber() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp()));
+ System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender().getNumber().get() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp()));
}
}
if (syncMessage.getRequest().isPresent()) {
@@ -113,6 +114,15 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (syncMessage.getRequest().get().isGroupsRequest()) {
System.out.println(" - groups request");
}
+ if (syncMessage.getRequest().get().isBlockedListRequest()) {
+ System.out.println(" - blocked list request");
+ }
+ if (syncMessage.getRequest().get().isConfigurationRequest()) {
+ System.out.println(" - configuration request");
+ }
+ if (syncMessage.getRequest().get().isKeysRequest()) {
+ System.out.println(" - keys request");
+ }
}
if (syncMessage.getSent().isPresent()) {
System.out.println("Received sync sent message");
@@ -122,6 +132,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
String dest = sentTranscriptMessage.getDestination().get().getNumber().get();
ContactInfo destContact = m.getContact(dest);
to = (destContact == null ? "" : "“" + destContact.name + "” ") + dest;
+ } else if (sentTranscriptMessage.getRecipients().size() > 0) {
+ StringBuilder toBuilder = new StringBuilder();
+ for (SignalServiceAddress dest : sentTranscriptMessage.getRecipients()) {
+ ContactInfo destContact = m.getContact(dest.getNumber().get());
+ toBuilder.append(destContact == null ? "" : "“" + destContact.name + "” ").append(dest.getNumber().get()).append(" ");
+ }
+ to = toBuilder.toString();
} else {
to = "Unknown";
}
@@ -137,14 +154,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Blocked numbers:");
final BlockedListMessage blockedList = syncMessage.getBlockedList().get();
for (SignalServiceAddress address : blockedList.getAddresses()) {
- System.out.println(" - " + address.getNumber());
+ System.out.println(" - " + address.getNumber().get());
}
}
if (syncMessage.getVerified().isPresent()) {
System.out.println("Received sync message with verified identities:");
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified());
- String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey()));
+ String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey()));
System.out.println(" " + safetyNumber);
}
if (syncMessage.getConfiguration().isPresent()) {
@@ -161,7 +178,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (syncMessage.getViewOnceOpen().isPresent()) {
final ViewOnceOpenMessage viewOnceOpenMessage = syncMessage.getViewOnceOpen().get();
System.out.println("Received sync message with view once open message:");
- System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber());
+ System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber().get());
System.out.println(" - Timestamp:" + viewOnceOpenMessage.getTimestamp());
}
if (syncMessage.getStickerPackOperations().isPresent()) {
@@ -183,7 +200,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
SignalServiceCallMessage callMessage = content.getCallMessage().get();
if (callMessage.getAnswerMessage().isPresent()) {
AnswerMessage answerMessage = callMessage.getAnswerMessage().get();
- System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getDescription());
+ System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getSdp());
}
if (callMessage.getBusyMessage().isPresent()) {
BusyMessage busyMessage = callMessage.getBusyMessage().get();
@@ -201,7 +218,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
if (callMessage.getOfferMessage().isPresent()) {
OfferMessage offerMessage = callMessage.getOfferMessage().get();
- System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getDescription());
+ System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getSdp());
}
}
if (content.getReceiptMessage().isPresent()) {
@@ -246,8 +263,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getBody().isPresent()) {
System.out.println("Body: " + message.getBody().get());
}
- if (message.getGroupInfo().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupInfo().get();
+ if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
@@ -311,10 +328,19 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Profile key update, key length:" + message.getProfileKey().get().length);
}
+ if (message.getReaction().isPresent()) {
+ final SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
+ System.out.println("Reaction:");
+ System.out.println(" - Emoji: " + reaction.getEmoji());
+ System.out.println(" - Target author: " + reaction.getTargetAuthor().getNumber().get());
+ System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp());
+ System.out.println(" - Is remove: " + reaction.isRemove());
+ }
+
if (message.getQuote().isPresent()) {
SignalServiceDataMessage.Quote quote = message.getQuote().get();
System.out.println("Quote: (" + quote.getId() + ")");
- System.out.println(" Author: " + quote.getAuthor().getNumber());
+ System.out.println(" Author: " + quote.getAuthor().getNumber().get());
System.out.println(" Text: " + quote.getText());
if (quote.getAttachments().size() > 0) {
System.out.println(" Attachments: ");
@@ -341,12 +367,12 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
if (attachment.isPointer()) {
final SignalServiceAttachmentPointer pointer = attachment.asPointer();
- System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length);
+ System.out.println(" Id: " + pointer.getRemoteId() + " Key length: " + pointer.getKey().length);
System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-"));
System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no"));
System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight());
- File file = m.getAttachmentFile(pointer.getId());
+ File file = m.getAttachmentFile(pointer.getRemoteId());
if (file.exists()) {
System.out.println(" Stored plaintext in: " + file);
}
diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java
index a49fc798..05f5c9ce 100644
--- a/src/main/java/org/asamk/signal/commands/BlockCommand.java
+++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java
@@ -2,9 +2,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
-import org.asamk.signal.GroupIdFormatException;
-import org.asamk.signal.GroupNotFoundException;
+
+import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java
index 1ad0987a..183b40a0 100644
--- a/src/main/java/org/asamk/signal/commands/Commands.java
+++ b/src/main/java/org/asamk/signal/commands/Commands.java
@@ -22,6 +22,7 @@ public class Commands {
addCommand("removeDevice", new RemoveDeviceCommand());
addCommand("removePin", new RemovePinCommand());
addCommand("send", new SendCommand());
+ addCommand("sendReaction", new SendReactionCommand());
addCommand("sendContacts", new SendContactsCommand());
addCommand("updateContact", new UpdateContactCommand());
addCommand("setPin", new SetPinCommand());
@@ -32,6 +33,7 @@ public class Commands {
addCommand("updateGroup", new UpdateGroupCommand());
addCommand("updateProfile", new UpdateProfileCommand());
addCommand("verify", new VerifyCommand());
+ addCommand("uploadStickerPack", new UploadStickerPackCommand());
}
public static Map getCommands() {
diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
index 6a3247d5..5cdb8ba1 100644
--- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java
+++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
@@ -6,8 +6,9 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.DbusReceiveMessageHandler;
import org.asamk.signal.JsonDbusReceiveMessageHandler;
+import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
-import org.freedesktop.dbus.DBusConnection;
+import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import java.io.IOException;
@@ -41,15 +42,15 @@ public class DaemonCommand implements LocalCommand {
DBusConnection conn = null;
try {
try {
- int busType;
+ DBusConnection.DBusBusType busType;
String busName;
if (ns.getBoolean("system")) {
- busType = DBusConnection.SYSTEM;
+ busType = DBusConnection.DBusBusType.SYSTEM;
} else {
- busType = DBusConnection.SESSION;
+ busType = DBusConnection.DBusBusType.SESSION;
}
conn = DBusConnection.getConnection(busType);
- conn.exportObject(SIGNAL_OBJECTPATH, m);
+ conn.exportObject(SIGNAL_OBJECTPATH, new DbusSignalImpl(m));
busName = ns.getString("busname");
if (busName == null) {
conn.requestBusName(SIGNAL_BUSNAME);
diff --git a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java
index b7f70dee..f9cd9de8 100644
--- a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java
@@ -3,7 +3,7 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
-import org.freedesktop.dbus.DBusConnection;
+import org.freedesktop.dbus.connections.impl.DBusConnection;
public interface ExtendedDbusCommand extends Command {
diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java
index 2a2d4c4b..45f59082 100644
--- a/src/main/java/org/asamk/signal/commands/LinkCommand.java
+++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java
@@ -3,8 +3,8 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
-import org.asamk.signal.UserAlreadyExists;
-import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.ProvisioningManager;
+import org.asamk.signal.manager.UserAlreadyExists;
import org.whispersystems.libsignal.InvalidKeyException;
import java.io.IOException;
@@ -12,7 +12,7 @@ import java.util.concurrent.TimeoutException;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
-public class LinkCommand implements LocalCommand {
+public class LinkCommand implements ProvisioningCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
@@ -21,15 +21,15 @@ public class LinkCommand implements LocalCommand {
}
@Override
- public int handleCommand(final Namespace ns, final Manager m) {
+ public int handleCommand(final Namespace ns, final ProvisioningManager m) {
String deviceName = ns.getString("name");
if (deviceName == null) {
deviceName = "cli";
}
try {
System.out.println(m.getDeviceLinkUri());
- m.finishDeviceLink(deviceName);
- System.out.println("Associated with: " + m.getUsername());
+ String username = m.finishDeviceLink(deviceName);
+ System.out.println("Associated with: " + username);
} catch (TimeoutException e) {
System.err.println("Link request timed out, please try again.");
return 3;
diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
index 1d2b7b31..24d6898c 100644
--- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
@@ -5,9 +5,11 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo;
+
import java.util.List;
public class ListContactsCommand implements LocalCommand {
+
@Override
public void attachToSubparser(final Subparser subparser) {
}
diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
index 9758b0e3..0baa8744 100644
--- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
@@ -6,19 +6,20 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
import java.util.List;
public class ListGroupsCommand implements LocalCommand {
- private static void printGroup(GroupInfo group, boolean detailed) {
+ private static void printGroup(GroupInfo group, boolean detailed, SignalServiceAddress address) {
if (detailed) {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s",
- Base64.encodeBytes(group.groupId), group.name, group.active, group.blocked, group.members));
+ Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked, group.getMembersE164()));
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
- Base64.encodeBytes(group.groupId), group.name, group.active, group.blocked));
+ Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked));
}
}
@@ -40,7 +41,7 @@ public class ListGroupsCommand implements LocalCommand {
boolean detailed = ns.getBoolean("detailed");
for (GroupInfo group : groups) {
- printGroup(group, detailed);
+ printGroup(group, detailed, m.getSelfAddress());
}
return 0;
}
diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
index dd3e5e46..edb67c76 100644
--- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
@@ -7,18 +7,16 @@ import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util;
-import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.List;
-import java.util.Map;
public class ListIdentitiesCommand implements LocalCommand {
- private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) {
- String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey()));
- System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername,
- theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits));
+ private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) {
+ String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
+ System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirId.getAddress().getNumber().orNull(),
+ theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits));
}
@Override
@@ -34,20 +32,18 @@ public class ListIdentitiesCommand implements LocalCommand {
return 1;
}
if (ns.get("number") == null) {
- for (Map.Entry> keys : m.getIdentities().entrySet()) {
- for (JsonIdentityKeyStore.Identity id : keys.getValue()) {
- printIdentityFingerprint(m, keys.getKey(), id);
- }
+ for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) {
+ printIdentityFingerprint(m, identity);
}
} else {
String number = ns.getString("number");
try {
- Pair> key = m.getIdentities(number);
- for (JsonIdentityKeyStore.Identity id : key.second()) {
- printIdentityFingerprint(m, key.first(), id);
+ List identities = m.getIdentities(number);
+ for (JsonIdentityKeyStore.Identity id : identities) {
+ printIdentityFingerprint(m, id);
}
} catch (InvalidNumberException e) {
- System.out.println("Invalid number: " + e.getMessage());
+ System.err.println("Invalid number: " + e.getMessage());
}
}
return 0;
diff --git a/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java b/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java
new file mode 100644
index 00000000..12a612ff
--- /dev/null
+++ b/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java
@@ -0,0 +1,10 @@
+package org.asamk.signal.commands;
+
+import net.sourceforge.argparse4j.inf.Namespace;
+
+import org.asamk.signal.manager.ProvisioningManager;
+
+public interface ProvisioningCommand extends Command {
+
+ int handleCommand(Namespace ns, ProvisioningManager m);
+}
diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
index 6e53cb2a..6db230f5 100644
--- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
+++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
@@ -3,10 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
-import org.asamk.signal.GroupIdFormatException;
-import org.asamk.signal.GroupNotFoundException;
-import org.asamk.signal.NotAGroupMemberException;
+import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.NotAGroupMemberException;
+import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java
index 876b6832..bc3acbde 100644
--- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java
@@ -1,5 +1,11 @@
package org.asamk.signal.commands;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
@@ -7,10 +13,10 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.JsonReceiveMessageHandler;
import org.asamk.signal.ReceiveMessageHandler;
+import org.asamk.signal.json.JsonMessageEnvelope;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.DateUtils;
-import org.freedesktop.dbus.DBusConnection;
-import org.freedesktop.dbus.DBusSigHandler;
+import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.util.Base64;
@@ -35,49 +41,102 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) {
- if (dbusconnection != null) {
- try {
- dbusconnection.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler() {
- @Override
- public void handle(Signal.MessageReceived s) {
- System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n",
- s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()), s.getMessage()));
- if (s.getGroupId().length > 0) {
- System.out.println("Group info:");
- System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId()));
- }
- if (s.getAttachments().size() > 0) {
- System.out.println("Attachments: ");
- for (String attachment : s.getAttachments()) {
- System.out.println("- Stored plaintext in: " + attachment);
- }
- }
+ final ObjectMapper jsonProcessor;
+ if (ns.getBoolean("json")) {
+ jsonProcessor = new ObjectMapper();
+ jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
+ jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
+ } else {
+ jsonProcessor = null;
+ }
+ try {
+ dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> {
+ if (jsonProcessor != null) {
+ JsonMessageEnvelope envelope = new JsonMessageEnvelope(messageReceived);
+ ObjectNode result = jsonProcessor.createObjectNode();
+ result.putPOJO("envelope", envelope);
+ try {
+ jsonProcessor.writeValue(System.out, result);
System.out.println();
+ } catch (IOException e) {
+ e.printStackTrace();
}
- });
- dbusconnection.addSigHandler(Signal.ReceiptReceived.class, new DBusSigHandler() {
- @Override
- public void handle(Signal.ReceiptReceived s) {
- System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
- s.getSender(), DateUtils.formatTimestamp(s.getTimestamp())));
+ } else {
+ System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n",
+ messageReceived.getSender(), DateUtils.formatTimestamp(messageReceived.getTimestamp()), messageReceived.getMessage()));
+ if (messageReceived.getGroupId().length > 0) {
+ System.out.println("Group info:");
+ System.out.println(" Id: " + Base64.encodeBytes(messageReceived.getGroupId()));
}
- });
- } catch (UnsatisfiedLinkError e) {
- System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
- return 1;
- } catch (DBusException e) {
- e.printStackTrace();
- return 1;
- }
- while (true) {
- try {
- Thread.sleep(10000);
- } catch (InterruptedException e) {
- return 0;
+ if (messageReceived.getAttachments().size() > 0) {
+ System.out.println("Attachments: ");
+ for (String attachment : messageReceived.getAttachments()) {
+ System.out.println("- Stored plaintext in: " + attachment);
+ }
+ }
+ System.out.println();
}
+ });
+
+ dbusconnection.addSigHandler(Signal.ReceiptReceived.class,
+ receiptReceived -> {
+ if (jsonProcessor != null) {
+ JsonMessageEnvelope envelope = new JsonMessageEnvelope(receiptReceived);
+ ObjectNode result = jsonProcessor.createObjectNode();
+ result.putPOJO("envelope", envelope);
+ try {
+ jsonProcessor.writeValue(System.out, result);
+ System.out.println();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
+ receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())));
+ }
+ });
+
+ dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> {
+ if (jsonProcessor != null) {
+ JsonMessageEnvelope envelope = new JsonMessageEnvelope(syncReceived);
+ ObjectNode result = jsonProcessor.createObjectNode();
+ result.putPOJO("envelope", envelope);
+ try {
+ jsonProcessor.writeValue(System.out, result);
+ System.out.println();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ System.out.print(String.format("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n",
+ syncReceived.getSource(), syncReceived.getDestination(), DateUtils.formatTimestamp(syncReceived.getTimestamp()), syncReceived.getMessage()));
+ if (syncReceived.getGroupId().length > 0) {
+ System.out.println("Group info:");
+ System.out.println(" Id: " + Base64.encodeBytes(syncReceived.getGroupId()));
+ }
+ if (syncReceived.getAttachments().size() > 0) {
+ System.out.println("Attachments: ");
+ for (String attachment : syncReceived.getAttachments()) {
+ System.out.println("- Stored plaintext in: " + attachment);
+ }
+ }
+ System.out.println();
+ }
+ });
+ } catch (UnsatisfiedLinkError e) {
+ System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
+ return 1;
+ } catch (DBusException e) {
+ e.printStackTrace();
+ return 1;
+ }
+ while (true) {
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ return 0;
}
}
- return 0;
}
@Override
diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java
index 2e2b7c4f..e95487bf 100644
--- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java
+++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java
@@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
+import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import java.io.IOException;
@@ -22,6 +23,9 @@ public class RegisterCommand implements LocalCommand {
try {
m.register(ns.getBoolean("voice"));
return 0;
+ } catch (CaptchaRequiredException e) {
+ System.err.println("Captcha required for verification (" + e.getMessage() + ")");
+ return 1;
} catch (IOException e) {
System.err.println("Request verify error: " + e.getMessage());
return 3;
diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java
index 39eb1f1b..b7de5402 100644
--- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java
+++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java
@@ -21,7 +21,7 @@ public class RemovePinCommand implements LocalCommand {
return 1;
}
try {
- m.setRegistrationLockPin(Optional.absent());
+ m.setRegistrationLockPin(Optional.absent());
return 0;
} catch (IOException e) {
System.err.println("Remove pin error: " + e.getMessage());
diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java
index 7320d76d..43166b5b 100644
--- a/src/main/java/org/asamk/signal/commands/SendCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendCommand.java
@@ -5,14 +5,10 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
-import org.asamk.signal.AttachmentInvalidException;
-import org.asamk.signal.GroupIdFormatException;
-import org.asamk.signal.GroupNotFoundException;
-import org.asamk.signal.NotAGroupMemberException;
+import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
-import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -20,12 +16,7 @@ import java.util.ArrayList;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
-import static org.asamk.signal.util.ErrorUtils.handleDBusExecutionException;
-import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
-import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
-import static org.asamk.signal.util.ErrorUtils.handleIOException;
-import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class SendCommand implements DbusCommand {
@@ -61,19 +52,13 @@ public class SendCommand implements DbusCommand {
if (ns.getBoolean("endsession")) {
try {
- signal.sendEndSessionMessage(ns.getList("recipient"));
+ signal.sendEndSessionMessage(ns.getList("recipient"));
return 0;
- } catch (IOException e) {
- handleIOException(e);
- return 3;
- } catch (EncapsulatedExceptions e) {
- handleEncapsulatedExceptions(e);
- return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (DBusExecutionException e) {
- handleDBusExecutionException(e);
+ System.err.println("Failed to send message: " + e.getMessage());
return 1;
}
}
@@ -89,42 +74,42 @@ public class SendCommand implements DbusCommand {
}
}
+ List attachments = ns.getList("attachment");
+ if (attachments == null) {
+ attachments = new ArrayList<>();
+ }
+
try {
- List attachments = ns.getList("attachment");
- if (attachments == null) {
- attachments = new ArrayList<>();
- }
if (ns.getString("group") != null) {
- byte[] groupId = Util.decodeGroupId(ns.getString("group"));
- signal.sendGroupMessage(messageText, attachments, groupId);
- } else {
- signal.sendMessage(messageText, attachments, ns.getList("recipient"));
+ byte[] groupId;
+ try {
+ groupId = Util.decodeGroupId(ns.getString("group"));
+ } catch (GroupIdFormatException e) {
+ handleGroupIdFormatException(e);
+ return 1;
+ }
+
+ long timestamp = signal.sendGroupMessage(messageText, attachments, groupId);
+ System.out.println(timestamp);
+ return 0;
}
- return 0;
- } catch (IOException e) {
- handleIOException(e);
- return 3;
- } catch (EncapsulatedExceptions e) {
- handleEncapsulatedExceptions(e);
- return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
- } catch (GroupNotFoundException e) {
- handleGroupNotFoundException(e);
+ } catch (DBusExecutionException e) {
+ System.err.println("Failed to send message: " + e.getMessage());
return 1;
- } catch (NotAGroupMemberException e) {
- handleNotAGroupMemberException(e);
- return 1;
- } catch (AttachmentInvalidException e) {
- System.err.println("Failed to add attachment: " + e.getMessage());
- System.err.println("Aborting sending.");
+ }
+
+ try {
+ long timestamp = signal.sendMessage(messageText, attachments, ns.getList("recipient"));
+ System.out.println(timestamp);
+ return 0;
+ } catch (AssertionError e) {
+ handleAssertionError(e);
return 1;
} catch (DBusExecutionException e) {
- handleDBusExecutionException(e);
- return 1;
- } catch (GroupIdFormatException e) {
- handleGroupIdFormatException(e);
+ System.err.println("Failed to send message: " + e.getMessage());
return 1;
}
}
diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java
index 523292ab..20e81a60 100644
--- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java
@@ -2,6 +2,7 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
+
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
new file mode 100644
index 00000000..7e748866
--- /dev/null
+++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
@@ -0,0 +1,99 @@
+package org.asamk.signal.commands;
+
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import org.asamk.signal.manager.GroupNotFoundException;
+import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.NotAGroupMemberException;
+import org.asamk.signal.util.GroupIdFormatException;
+import org.asamk.signal.util.Util;
+import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
+import org.whispersystems.signalservice.api.util.InvalidNumberException;
+
+import java.io.IOException;
+
+import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
+import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
+import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
+import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
+import static org.asamk.signal.util.ErrorUtils.handleIOException;
+import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
+import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
+
+public class SendReactionCommand implements LocalCommand {
+
+ @Override
+ public void attachToSubparser(final Subparser subparser) {
+ subparser.help("Send reaction to a previously received or sent message.");
+ subparser.addArgument("-g", "--group")
+ .help("Specify the recipient group ID.");
+ subparser.addArgument("recipient")
+ .help("Specify the recipients' phone number.")
+ .nargs("*");
+ subparser.addArgument("-e", "--emoji")
+ .required(true)
+ .help("Specify the emoji, should be a single unicode grapheme cluster.");
+ subparser.addArgument("-a", "--target-author")
+ .required(true)
+ .help("Specify the number of the author of the message to which to react.");
+ subparser.addArgument("-t", "--target-timestamp")
+ .required(true)
+ .type(long.class)
+ .help("Specify the timestamp of the message to which to react.");
+ subparser.addArgument("-r", "--remove")
+ .help("Remove a reaction.")
+ .action(Arguments.storeTrue());
+ }
+
+ @Override
+ public int handleCommand(final Namespace ns, final Manager m) {
+ if (!m.isRegistered()) {
+ System.err.println("User is not registered.");
+ return 1;
+ }
+
+ if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && ns.getString("group") == null) {
+ System.err.println("No recipients given");
+ System.err.println("Aborting sending.");
+ return 1;
+ }
+
+ String emoji = ns.getString("emoji");
+ boolean isRemove = ns.getBoolean("remove");
+ String targetAuthor = ns.getString("target_author");
+ long targetTimestamp = ns.getLong("target_timestamp");
+
+ try {
+ if (ns.getString("group") != null) {
+ byte[] groupId = Util.decodeGroupId(ns.getString("group"));
+ m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
+ } else {
+ m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient"));
+ }
+ return 0;
+ } catch (IOException e) {
+ handleIOException(e);
+ return 3;
+ } catch (EncapsulatedExceptions e) {
+ handleEncapsulatedExceptions(e);
+ return 3;
+ } catch (AssertionError e) {
+ handleAssertionError(e);
+ return 1;
+ } catch (GroupNotFoundException e) {
+ handleGroupNotFoundException(e);
+ return 1;
+ } catch (NotAGroupMemberException e) {
+ handleNotAGroupMemberException(e);
+ return 1;
+ } catch (GroupIdFormatException e) {
+ handleGroupIdFormatException(e);
+ return 1;
+ } catch (InvalidNumberException e) {
+ handleInvalidNumberException(e);
+ return 1;
+ }
+ }
+}
diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java
index f2744545..2780dc46 100644
--- a/src/main/java/org/asamk/signal/commands/TrustCommand.java
+++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java
@@ -6,7 +6,9 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.util.ErrorUtils;
import org.asamk.signal.util.Hex;
+import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.Locale;
@@ -21,8 +23,8 @@ public class TrustCommand implements LocalCommand {
mutTrust.addArgument("-a", "--trust-all-known-keys")
.help("Trust all known keys of this user, only use this for testing.")
.action(Arguments.storeTrue());
- mutTrust.addArgument("-v", "--verified-fingerprint")
- .help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint.");
+ mutTrust.addArgument("-v", "--verified-safety-number", "--verified-fingerprint")
+ .help("Specify the safety number of the key, only use this option if you have verified the safety number.");
}
@Override
@@ -39,34 +41,46 @@ public class TrustCommand implements LocalCommand {
return 1;
}
} else {
- String fingerprint = ns.getString("verified_fingerprint");
- if (fingerprint != null) {
- fingerprint = fingerprint.replaceAll(" ", "");
- if (fingerprint.length() == 66) {
+ String safetyNumber = ns.getString("verified_safety_number");
+ if (safetyNumber != null) {
+ safetyNumber = safetyNumber.replaceAll(" ", "");
+ if (safetyNumber.length() == 66) {
byte[] fingerprintBytes;
try {
- fingerprintBytes = Hex.toByteArray(fingerprint.toLowerCase(Locale.ROOT));
+ fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT));
} catch (Exception e) {
System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters.");
return 1;
}
- boolean res = m.trustIdentityVerified(number, fingerprintBytes);
+ boolean res;
+ try {
+ res = m.trustIdentityVerified(number, fingerprintBytes);
+ } catch (InvalidNumberException e) {
+ ErrorUtils.handleInvalidNumberException(e);
+ return 1;
+ }
if (!res) {
System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
return 1;
}
- } else if (fingerprint.length() == 60) {
- boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint);
+ } else if (safetyNumber.length() == 60) {
+ boolean res;
+ try {
+ res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber);
+ } catch (InvalidNumberException e) {
+ ErrorUtils.handleInvalidNumberException(e);
+ return 1;
+ }
if (!res) {
System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct.");
return 1;
}
} else {
- System.err.println("Fingerprint has invalid format, either specify the old hex fingerprint or the new safety number");
+ System.err.println("Safety number has invalid format, either specify the old hex fingerprint or the new safety number");
return 1;
}
} else {
- System.err.println("You need to specify the fingerprint you have verified with -v FINGERPRINT");
+ System.err.println("You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER");
return 1;
}
}
diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java
index be745cb0..a95aa328 100644
--- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java
@@ -2,9 +2,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
-import org.asamk.signal.GroupIdFormatException;
-import org.asamk.signal.GroupNotFoundException;
+
+import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
index 77f38e5e..d7fa3893 100644
--- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
@@ -6,6 +6,8 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
+import java.io.IOException;
+
public class UpdateContactCommand implements LocalCommand {
@Override
@@ -15,6 +17,10 @@ public class UpdateContactCommand implements LocalCommand {
subparser.addArgument("-n", "--name")
.required(true)
.help("New contact name");
+ subparser.addArgument("-e", "--expiration")
+ .required(false)
+ .type(int.class)
+ .help("Set expiration time of messages (seconds)");
subparser.help("Update the details of a given contact");
}
@@ -30,8 +36,17 @@ public class UpdateContactCommand implements LocalCommand {
try {
m.setContactName(number, name);
+
+ Integer expiration = ns.getInt("expiration");
+ if (expiration != null) {
+ m.setExpirationTimer(number, expiration);
+ }
} catch (InvalidNumberException e) {
- System.out.println("Invalid contact number: " + e.getMessage());
+ System.err.println("Invalid contact number: " + e.getMessage());
+ return 1;
+ } catch (IOException e) {
+ System.err.println("Update contact error: " + e.getMessage());
+ return 3;
}
return 0;
diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
index 66071cb0..925b8c90 100644
--- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
@@ -4,23 +4,16 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
-import org.asamk.signal.AttachmentInvalidException;
-import org.asamk.signal.GroupIdFormatException;
-import org.asamk.signal.GroupNotFoundException;
-import org.asamk.signal.NotAGroupMemberException;
+import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
-import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
+import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
-import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
+import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
-import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
-import static org.asamk.signal.util.ErrorUtils.handleIOException;
-import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class UpdateGroupCommand implements DbusCommand {
@@ -44,49 +37,48 @@ public class UpdateGroupCommand implements DbusCommand {
return 1;
}
- try {
- byte[] groupId = null;
- if (ns.getString("group") != null) {
+ byte[] groupId = null;
+ if (ns.getString("group") != null) {
+ try {
groupId = Util.decodeGroupId(ns.getString("group"));
+ } catch (GroupIdFormatException e) {
+ handleGroupIdFormatException(e);
+ return 1;
}
- if (groupId == null) {
- groupId = new byte[0];
- }
- String groupName = ns.getString("name");
- if (groupName == null) {
- groupName = "";
- }
- List groupMembers = ns.getList("member");
- if (groupMembers == null) {
- groupMembers = new ArrayList<>();
- }
- String groupAvatar = ns.getString("avatar");
- if (groupAvatar == null) {
- groupAvatar = "";
- }
+ }
+ if (groupId == null) {
+ groupId = new byte[0];
+ }
+
+ String groupName = ns.getString("name");
+ if (groupName == null) {
+ groupName = "";
+ }
+
+ List groupMembers = ns.getList("member");
+ if (groupMembers == null) {
+ groupMembers = new ArrayList<>();
+ }
+
+ String groupAvatar = ns.getString("avatar");
+ if (groupAvatar == null) {
+ groupAvatar = "";
+ }
+
+ try {
byte[] newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar);
if (groupId.length != newGroupId.length) {
System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …");
}
return 0;
- } catch (IOException e) {
- handleIOException(e);
- return 3;
- } catch (AttachmentInvalidException e) {
+ } catch (AssertionError e) {
+ handleAssertionError(e);
+ return 1;
+ } catch (Signal.Error.AttachmentInvalid e) {
System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
- System.err.println("Aborting sending.");
return 1;
- } catch (GroupNotFoundException e) {
- handleGroupNotFoundException(e);
- return 1;
- } catch (NotAGroupMemberException e) {
- handleNotAGroupMemberException(e);
- return 1;
- } catch (EncapsulatedExceptions e) {
- handleEncapsulatedExceptions(e);
- return 3;
- } catch (GroupIdFormatException e) {
- handleGroupIdFormatException(e);
+ } catch (DBusExecutionException e) {
+ System.err.println("Failed to send message: " + e.getMessage());
return 1;
}
}
diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java
index a7b02937..218c8b77 100644
--- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java
@@ -14,16 +14,18 @@ public class UpdateProfileCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup();
+ final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup()
+ .required(true);
avatarOptions.addArgument("--avatar")
.help("Path to new profile avatar");
avatarOptions.addArgument("--remove-avatar")
.action(Arguments.storeTrue());
subparser.addArgument("--name")
+ .required(true)
.help("New profile name");
- subparser.help("Set a name and/or avatar image for the user profile");
+ subparser.help("Set a name and avatar image for the user profile");
}
@Override
@@ -34,38 +36,15 @@ public class UpdateProfileCommand implements LocalCommand {
}
String name = ns.getString("name");
-
- if (name != null) {
- try {
- m.setProfileName(name);
- } catch (IOException e) {
- System.err.println("UpdateAccount error: " + e.getMessage());
- return 3;
- }
- }
-
String avatarPath = ns.getString("avatar");
-
- if (avatarPath != null) {
- File avatarFile = new File(avatarPath);
-
- try {
- m.setProfileAvatar(avatarFile);
- } catch (IOException e) {
- System.err.println("UpdateAccount error: " + e.getMessage());
- return 3;
- }
- }
-
boolean removeAvatar = ns.getBoolean("remove_avatar");
- if (removeAvatar) {
- try {
- m.removeProfileAvatar();
- } catch (IOException e) {
- System.err.println("UpdateAccount error: " + e.getMessage());
- return 3;
- }
+ try {
+ File avatarFile = removeAvatar ? null : new File(avatarPath);
+ m.setProfile(name, avatarFile);
+ } catch (IOException e) {
+ System.err.println("UpdateAccount error: " + e.getMessage());
+ return 3;
}
return 0;
diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java
new file mode 100644
index 00000000..77df2b22
--- /dev/null
+++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java
@@ -0,0 +1,34 @@
+package org.asamk.signal.commands;
+
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.StickerPackInvalidException;
+
+import java.io.IOException;
+
+public class UploadStickerPackCommand implements LocalCommand {
+
+ @Override
+ public void attachToSubparser(final Subparser subparser) {
+ subparser.addArgument("path")
+ .help("The path of the manifest.json or a zip file containing the sticker pack you wish to upload.");
+ }
+
+ @Override
+ public int handleCommand(final Namespace ns, final Manager m) {
+ try {
+ String path = ns.getString("path");
+ String url = m.uploadStickerPack(path);
+ System.out.println(url);
+ return 0;
+ } catch (IOException e) {
+ System.err.println("Upload error: " + e.getMessage());
+ return 3;
+ } catch (StickerPackInvalidException e) {
+ System.err.println("Invalid sticker pack: " + e.getMessage());
+ return 3;
+ }
+ }
+}
diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java
index 9d99c0d2..0f336325 100644
--- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java
+++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java
@@ -20,10 +20,6 @@ public class VerifyCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
- if (!m.userHasKeys()) {
- System.err.println("User has no keys, first call register.");
- return 1;
- }
if (m.isRegistered()) {
System.err.println("User registration is already verified");
return 1;
diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
new file mode 100644
index 00000000..17cc2caa
--- /dev/null
+++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
@@ -0,0 +1,205 @@
+package org.asamk.signal.dbus;
+
+import org.asamk.Signal;
+import org.asamk.signal.manager.AttachmentInvalidException;
+import org.asamk.signal.manager.GroupNotFoundException;
+import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.NotAGroupMemberException;
+import org.asamk.signal.storage.groups.GroupInfo;
+import org.freedesktop.dbus.exceptions.DBusExecutionException;
+import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
+import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
+import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
+import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
+import org.whispersystems.signalservice.api.util.InvalidNumberException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class DbusSignalImpl implements Signal {
+
+ private final Manager m;
+
+ public DbusSignalImpl(final Manager m) {
+ this.m = m;
+ }
+
+ @Override
+ public boolean isRemote() {
+ return false;
+ }
+
+ @Override
+ public String getObjectPath() {
+ return null;
+ }
+
+ @Override
+ public long sendMessage(final String message, final List attachments, final String recipient) {
+ List recipients = new ArrayList<>(1);
+ recipients.add(recipient);
+ return sendMessage(message, attachments, recipients);
+ }
+
+ private static DBusExecutionException convertEncapsulatedExceptions(EncapsulatedExceptions e) {
+ if (e.getNetworkExceptions().size() + e.getUnregisteredUserExceptions().size() + e.getUntrustedIdentityExceptions().size() == 1) {
+ if (e.getNetworkExceptions().size() == 1) {
+ NetworkFailureException n = e.getNetworkExceptions().get(0);
+ return new Error.Failure("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
+ } else if (e.getUnregisteredUserExceptions().size() == 1) {
+ UnregisteredUserException n = e.getUnregisteredUserExceptions().get(0);
+ return new Error.UnregisteredUser("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
+ } else if (e.getUntrustedIdentityExceptions().size() == 1) {
+ UntrustedIdentityException n = e.getUntrustedIdentityExceptions().get(0);
+ return new Error.UntrustedIdentity("Untrusted Identity for \"" + n.getIdentifier() + "\": " + n.getMessage());
+ }
+ }
+
+ StringBuilder message = new StringBuilder();
+ message.append("Failed to send (some) messages:").append('\n');
+ for (NetworkFailureException n : e.getNetworkExceptions()) {
+ message.append("Network failure for \"").append(n.getE164number()).append("\": ").append(n.getMessage()).append('\n');
+ }
+ for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
+ message.append("Unregistered user \"").append(n.getE164Number()).append("\": ").append(n.getMessage()).append('\n');
+ }
+ for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
+ message.append("Untrusted Identity for \"").append(n.getIdentifier()).append("\": ").append(n.getMessage()).append('\n');
+ }
+
+ return new Error.Failure(message.toString());
+ }
+
+ @Override
+ public long sendMessage(final String message, final List attachments, final List recipients) {
+ try {
+ return m.sendMessage(message, attachments, recipients);
+ } catch (EncapsulatedExceptions e) {
+ throw convertEncapsulatedExceptions(e);
+ } catch (InvalidNumberException e) {
+ throw new Error.InvalidNumber(e.getMessage());
+ } catch (AttachmentInvalidException e) {
+ throw new Error.AttachmentInvalid(e.getMessage());
+ } catch (IOException e) {
+ throw new Error.Failure(e.getMessage());
+ }
+ }
+
+ @Override
+ public void sendEndSessionMessage(final List recipients) {
+ try {
+ m.sendEndSessionMessage(recipients);
+ } catch (IOException e) {
+ throw new Error.Failure(e.getMessage());
+ } catch (EncapsulatedExceptions e) {
+ throw convertEncapsulatedExceptions(e);
+ } catch (InvalidNumberException e) {
+ throw new Error.InvalidNumber(e.getMessage());
+ }
+ }
+
+ @Override
+ public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) {
+ try {
+ return m.sendGroupMessage(message, attachments, groupId);
+ } catch (IOException e) {
+ throw new Error.Failure(e.getMessage());
+ } catch (EncapsulatedExceptions e) {
+ throw convertEncapsulatedExceptions(e);
+ } catch (GroupNotFoundException | NotAGroupMemberException e) {
+ throw new Error.GroupNotFound(e.getMessage());
+ } catch (AttachmentInvalidException e) {
+ throw new Error.AttachmentInvalid(e.getMessage());
+ }
+ }
+
+ @Override
+ public String getContactName(final String number) {
+ try {
+ return m.getContactName(number);
+ } catch (InvalidNumberException e) {
+ throw new Error.InvalidNumber(e.getMessage());
+ }
+ }
+
+ @Override
+ public void setContactName(final String number, final String name) {
+ try {
+ m.setContactName(number, name);
+ } catch (InvalidNumberException e) {
+ throw new Error.InvalidNumber(e.getMessage());
+ }
+ }
+
+ @Override
+ public void setContactBlocked(final String number, final boolean blocked) {
+ try {
+ m.setContactBlocked(number, blocked);
+ } catch (InvalidNumberException e) {
+ throw new Error.InvalidNumber(e.getMessage());
+ }
+ }
+
+ @Override
+ public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
+ try {
+ m.setGroupBlocked(groupId, blocked);
+ } catch (GroupNotFoundException e) {
+ throw new Error.GroupNotFound(e.getMessage());
+ }
+ }
+
+ @Override
+ public List getGroupIds() {
+ List groups = m.getGroups();
+ List ids = new ArrayList<>(groups.size());
+ for (GroupInfo group : groups) {
+ ids.add(group.groupId);
+ }
+ return ids;
+ }
+
+ @Override
+ public String getGroupName(final byte[] groupId) {
+ GroupInfo group = m.getGroup(groupId);
+ if (group == null) {
+ return "";
+ } else {
+ return group.name;
+ }
+ }
+
+ @Override
+ public List getGroupMembers(final byte[] groupId) {
+ GroupInfo group = m.getGroup(groupId);
+ if (group == null) {
+ return Collections.emptyList();
+ } else {
+ return new ArrayList<>(group.getMembersE164());
+ }
+ }
+
+ @Override
+ public byte[] updateGroup(final byte[] groupId, final String name, final List members, final String avatar) {
+ try {
+ return m.updateGroup(groupId, name, members, avatar);
+ } catch (IOException e) {
+ throw new Error.Failure(e.getMessage());
+ } catch (EncapsulatedExceptions e) {
+ throw convertEncapsulatedExceptions(e);
+ } catch (GroupNotFoundException | NotAGroupMemberException e) {
+ throw new Error.GroupNotFound(e.getMessage());
+ } catch (InvalidNumberException e) {
+ throw new Error.InvalidNumber(e.getMessage());
+ } catch (AttachmentInvalidException e) {
+ throw new Error.AttachmentInvalid(e.getMessage());
+ }
+ }
+
+ @Override
+ public boolean isRegistered() {
+ return true;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/JsonAttachment.java b/src/main/java/org/asamk/signal/json/JsonAttachment.java
similarity index 81%
rename from src/main/java/org/asamk/signal/JsonAttachment.java
rename to src/main/java/org/asamk/signal/json/JsonAttachment.java
index 58165639..1949171a 100644
--- a/src/main/java/org/asamk/signal/JsonAttachment.java
+++ b/src/main/java/org/asamk/signal/json/JsonAttachment.java
@@ -1,4 +1,4 @@
-package org.asamk.signal;
+package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@@ -15,7 +15,7 @@ class JsonAttachment {
final SignalServiceAttachmentPointer pointer = attachment.asPointer();
if (attachment.isPointer()) {
- this.id = String.valueOf(pointer.getId());
+ this.id = String.valueOf(pointer.getRemoteId());
if (pointer.getFileName().isPresent()) {
this.filename = pointer.getFileName().get();
}
@@ -24,4 +24,8 @@ class JsonAttachment {
}
}
}
+
+ JsonAttachment(String filename) {
+ this.filename = filename;
+ }
}
diff --git a/src/main/java/org/asamk/signal/JsonCallMessage.java b/src/main/java/org/asamk/signal/json/JsonCallMessage.java
similarity index 97%
rename from src/main/java/org/asamk/signal/JsonCallMessage.java
rename to src/main/java/org/asamk/signal/json/JsonCallMessage.java
index 2c8518f9..c1b1d443 100644
--- a/src/main/java/org/asamk/signal/JsonCallMessage.java
+++ b/src/main/java/org/asamk/signal/json/JsonCallMessage.java
@@ -1,4 +1,4 @@
-package org.asamk.signal;
+package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java
new file mode 100644
index 00000000..fc8538aa
--- /dev/null
+++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java
@@ -0,0 +1,59 @@
+package org.asamk.signal.json;
+
+import org.asamk.Signal;
+import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
+import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+class JsonDataMessage {
+
+ long timestamp;
+ String message;
+ int expiresInSeconds;
+ List attachments;
+ JsonGroupInfo groupInfo;
+
+ JsonDataMessage(SignalServiceDataMessage dataMessage) {
+ this.timestamp = dataMessage.getTimestamp();
+ if (dataMessage.getGroupContext().isPresent() && dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get();
+ this.groupInfo = new JsonGroupInfo(groupInfo);
+ }
+ if (dataMessage.getBody().isPresent()) {
+ this.message = dataMessage.getBody().get();
+ }
+ this.expiresInSeconds = dataMessage.getExpiresInSeconds();
+ if (dataMessage.getAttachments().isPresent()) {
+ this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size());
+ for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) {
+ this.attachments.add(new JsonAttachment(attachment));
+ }
+ } else {
+ this.attachments = new ArrayList<>();
+ }
+ }
+
+ public JsonDataMessage(Signal.MessageReceived messageReceived) {
+ timestamp = messageReceived.getTimestamp();
+ message = messageReceived.getMessage();
+ groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
+ attachments = messageReceived.getAttachments()
+ .stream()
+ .map(JsonAttachment::new)
+ .collect(Collectors.toList());
+ }
+
+ public JsonDataMessage(Signal.SyncMessageReceived messageReceived) {
+ timestamp = messageReceived.getTimestamp();
+ message = messageReceived.getMessage();
+ groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
+ attachments = messageReceived.getAttachments()
+ .stream()
+ .map(JsonAttachment::new)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/org/asamk/signal/json/JsonError.java b/src/main/java/org/asamk/signal/json/JsonError.java
new file mode 100644
index 00000000..29d85c8b
--- /dev/null
+++ b/src/main/java/org/asamk/signal/json/JsonError.java
@@ -0,0 +1,10 @@
+package org.asamk.signal.json;
+
+public class JsonError {
+
+ String message;
+
+ public JsonError(Throwable exception) {
+ this.message = exception.getMessage();
+ }
+}
diff --git a/src/main/java/org/asamk/signal/JsonGroupInfo.java b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java
similarity index 87%
rename from src/main/java/org/asamk/signal/JsonGroupInfo.java
rename to src/main/java/org/asamk/signal/json/JsonGroupInfo.java
index 5678b896..572623e4 100644
--- a/src/main/java/org/asamk/signal/JsonGroupInfo.java
+++ b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java
@@ -1,4 +1,4 @@
-package org.asamk.signal;
+package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -27,4 +27,8 @@ class JsonGroupInfo {
}
this.type = groupInfo.getType().toString();
}
+
+ JsonGroupInfo(byte[] groupId) {
+ this.groupId = Base64.encodeBytes(groupId);
+ }
}
diff --git a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
similarity index 71%
rename from src/main/java/org/asamk/signal/JsonMessageEnvelope.java
rename to src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
index 9a275970..3279d941 100644
--- a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java
+++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
@@ -1,10 +1,11 @@
-package org.asamk.signal;
+package org.asamk.signal.json;
+import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-class JsonMessageEnvelope {
+public class JsonMessageEnvelope {
String source;
int sourceDevice;
@@ -44,4 +45,22 @@ class JsonMessageEnvelope {
}
}
}
+
+ public JsonMessageEnvelope(Signal.MessageReceived messageReceived) {
+ source = messageReceived.getSender();
+ timestamp = messageReceived.getTimestamp();
+ dataMessage = new JsonDataMessage(messageReceived);
+ }
+
+ public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) {
+ source = receiptReceived.getSender();
+ timestamp = receiptReceived.getTimestamp();
+ isReceipt = true;
+ }
+
+ public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) {
+ source = messageReceived.getSource();
+ timestamp = messageReceived.getTimestamp();
+ syncMessage = new JsonSyncMessage(messageReceived);
+ }
}
diff --git a/src/main/java/org/asamk/signal/JsonReceiptMessage.java b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
similarity index 95%
rename from src/main/java/org/asamk/signal/JsonReceiptMessage.java
rename to src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
index fd875af5..1b896053 100644
--- a/src/main/java/org/asamk/signal/JsonReceiptMessage.java
+++ b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
@@ -1,4 +1,4 @@
-package org.asamk.signal;
+package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
diff --git a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java
similarity index 67%
rename from src/main/java/org/asamk/signal/JsonSyncDataMessage.java
rename to src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java
index b72fb26d..d253b197 100644
--- a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java
+++ b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java
@@ -1,5 +1,6 @@
-package org.asamk.signal;
+package org.asamk.signal.json;
+import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
class JsonSyncDataMessage extends JsonDataMessage {
@@ -12,4 +13,9 @@ class JsonSyncDataMessage extends JsonDataMessage {
this.destination = transcriptMessage.getDestination().get().getNumber().get();
}
}
+
+ JsonSyncDataMessage(Signal.SyncMessageReceived messageReceived) {
+ super(messageReceived);
+ destination = messageReceived.getDestination();
+ }
}
diff --git a/src/main/java/org/asamk/signal/json/JsonSyncMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java
new file mode 100644
index 00000000..27766bda
--- /dev/null
+++ b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java
@@ -0,0 +1,50 @@
+package org.asamk.signal.json;
+
+import org.asamk.Signal;
+import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+import java.util.ArrayList;
+import java.util.List;
+
+enum JsonSyncMessageType {
+ CONTACTS_SYNC,
+ GROUPS_SYNC,
+ REQUEST_SYNC
+}
+
+class JsonSyncMessage {
+
+ JsonSyncDataMessage sentMessage;
+ List blockedNumbers;
+ List readMessages;
+ JsonSyncMessageType type;
+
+ JsonSyncMessage(SignalServiceSyncMessage syncMessage) {
+ if (syncMessage.getSent().isPresent()) {
+ this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get());
+ }
+ if (syncMessage.getBlockedList().isPresent()) {
+ this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size());
+ for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) {
+ this.blockedNumbers.add(address.getNumber().get());
+ }
+ }
+ if (syncMessage.getRead().isPresent()) {
+ this.readMessages = syncMessage.getRead().get();
+ }
+
+ if (syncMessage.getContacts().isPresent()) {
+ this.type = JsonSyncMessageType.CONTACTS_SYNC;
+ } else if (syncMessage.getGroups().isPresent()) {
+ this.type = JsonSyncMessageType.GROUPS_SYNC;
+ } else if (syncMessage.getRequest().isPresent()) {
+ this.type = JsonSyncMessageType.REQUEST_SYNC;
+ }
+ }
+
+ JsonSyncMessage(Signal.SyncMessageReceived messageReceived) {
+ sentMessage = new JsonSyncDataMessage(messageReceived);
+ }
+}
diff --git a/src/main/java/org/asamk/signal/AttachmentInvalidException.java b/src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java
similarity index 57%
rename from src/main/java/org/asamk/signal/AttachmentInvalidException.java
rename to src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java
index 839c7940..78fba6e0 100644
--- a/src/main/java/org/asamk/signal/AttachmentInvalidException.java
+++ b/src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java
@@ -1,8 +1,6 @@
-package org.asamk.signal;
+package org.asamk.signal.manager;
-import org.freedesktop.dbus.exceptions.DBusExecutionException;
-
-public class AttachmentInvalidException extends DBusExecutionException {
+public class AttachmentInvalidException extends Exception {
public AttachmentInvalidException(String message) {
super(message);
diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java
deleted file mode 100644
index 0f503352..00000000
--- a/src/main/java/org/asamk/signal/manager/BaseConfig.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.asamk.signal.manager;
-
-import org.whispersystems.signalservice.api.push.TrustStore;
-import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
-import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
-import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
-import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
-
-public class BaseConfig {
-
- public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
- public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
-
- final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION;
- final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
- final static int PREKEY_MINIMUM_COUNT = 20;
- final static int PREKEY_BATCH_SIZE = 100;
- final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
-
- private final static String URL = "https://textsecure-service.whispersystems.org";
- private final static String CDN_URL = "https://cdn.signal.org";
- private final static TrustStore TRUST_STORE = new WhisperTrustStore();
-
- final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration(
- new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
- new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
- new SignalContactDiscoveryUrl[0]
- );
-
- private BaseConfig() {
- }
-}
diff --git a/src/main/java/org/asamk/signal/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java
similarity index 51%
rename from src/main/java/org/asamk/signal/GroupNotFoundException.java
rename to src/main/java/org/asamk/signal/manager/GroupNotFoundException.java
index f09014a0..0c0d6d2d 100644
--- a/src/main/java/org/asamk/signal/GroupNotFoundException.java
+++ b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java
@@ -1,9 +1,8 @@
-package org.asamk.signal;
+package org.asamk.signal.manager;
-import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64;
-public class GroupNotFoundException extends DBusExecutionException {
+public class GroupNotFoundException extends Exception {
public GroupNotFoundException(byte[] groupId) {
super("Group not found: " + Base64.encodeBytes(groupId));
diff --git a/src/main/java/org/asamk/signal/manager/HandleAction.java b/src/main/java/org/asamk/signal/manager/HandleAction.java
new file mode 100644
index 00000000..2ef99062
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/HandleAction.java
@@ -0,0 +1,156 @@
+package org.asamk.signal.manager;
+
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+interface HandleAction {
+
+ void execute(Manager m) throws Throwable;
+}
+
+class SendReceiptAction implements HandleAction {
+
+ private final SignalServiceAddress address;
+ private final long timestamp;
+
+ public SendReceiptAction(final SignalServiceAddress address, final long timestamp) {
+ this.address = address;
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public void execute(Manager m) throws Throwable {
+ m.sendReceipt(address, timestamp);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final SendReceiptAction that = (SendReceiptAction) o;
+ return timestamp == that.timestamp &&
+ address.equals(that.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(address, timestamp);
+ }
+}
+
+class SendSyncContactsAction implements HandleAction {
+
+ private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction();
+
+ private SendSyncContactsAction() {
+ }
+
+ public static SendSyncContactsAction create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void execute(Manager m) throws Throwable {
+ m.sendContacts();
+ }
+}
+
+class SendSyncGroupsAction implements HandleAction {
+
+ private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction();
+
+ private SendSyncGroupsAction() {
+ }
+
+ public static SendSyncGroupsAction create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void execute(Manager m) throws Throwable {
+ m.sendGroups();
+ }
+}
+
+class SendSyncBlockedListAction implements HandleAction {
+
+ private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction();
+
+ private SendSyncBlockedListAction() {
+ }
+
+ public static SendSyncBlockedListAction create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void execute(Manager m) throws Throwable {
+ m.sendBlockedList();
+ }
+}
+
+class SendGroupInfoRequestAction implements HandleAction {
+
+ private final SignalServiceAddress address;
+ private final byte[] groupId;
+
+ public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) {
+ this.address = address;
+ this.groupId = groupId;
+ }
+
+ @Override
+ public void execute(Manager m) throws Throwable {
+ m.sendGroupInfoRequest(groupId, address);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o;
+ return address.equals(that.address) &&
+ Arrays.equals(groupId, that.groupId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(address);
+ result = 31 * result + Arrays.hashCode(groupId);
+ return result;
+ }
+}
+
+class SendGroupUpdateAction implements HandleAction {
+
+ private final SignalServiceAddress address;
+ private final byte[] groupId;
+
+ public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) {
+ this.address = address;
+ this.groupId = groupId;
+ }
+
+ @Override
+ public void execute(Manager m) throws Throwable {
+ m.sendUpdateGroupMessage(groupId, address);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final SendGroupUpdateAction that = (SendGroupUpdateAction) o;
+ return address.equals(that.address) &&
+ Arrays.equals(groupId, that.groupId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(address);
+ result = 31 * result + Arrays.hashCode(groupId);
+ return result;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/JsonStickerPack.java b/src/main/java/org/asamk/signal/manager/JsonStickerPack.java
new file mode 100644
index 00000000..a7e5eb7f
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/JsonStickerPack.java
@@ -0,0 +1,29 @@
+package org.asamk.signal.manager;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+class JsonStickerPack {
+
+ @JsonProperty
+ public String title;
+
+ @JsonProperty
+ public String author;
+
+ @JsonProperty
+ public JsonSticker cover;
+
+ @JsonProperty
+ public List stickers;
+
+ public static class JsonSticker {
+
+ @JsonProperty
+ public String emoji;
+
+ @JsonProperty
+ public String file;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java
index 6ffc3f36..fff8179c 100644
--- a/src/main/java/org/asamk/signal/manager/KeyUtils.java
+++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java
@@ -1,6 +1,8 @@
package org.asamk.signal.manager;
import org.asamk.signal.util.RandomUtils;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.util.Base64;
class KeyUtils {
@@ -12,8 +14,12 @@ class KeyUtils {
return getSecret(52);
}
- static byte[] createProfileKey() {
- return getSecretBytes(32);
+ static ProfileKey createProfileKey() {
+ try {
+ return new ProfileKey(getSecretBytes(32));
+ } catch (InvalidInputException e) {
+ throw new AssertionError("Profile key is guaranteed to be 32 bytes here");
+ }
}
static String createPassword() {
@@ -24,6 +30,14 @@ class KeyUtils {
return getSecretBytes(16);
}
+ static byte[] createUnrestrictedUnidentifiedAccess() {
+ return getSecretBytes(16);
+ }
+
+ static byte[] createStickerUploadKey() {
+ return getSecretBytes(32);
+ }
+
private static String getSecret(int size) {
byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret);
diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java
index 59061068..2ce59cdc 100644
--- a/src/main/java/org/asamk/signal/manager/Manager.java
+++ b/src/main/java/org/asamk/signal/manager/Manager.java
@@ -16,18 +16,15 @@
*/
package org.asamk.signal.manager;
-import org.asamk.Signal;
-import org.asamk.signal.AttachmentInvalidException;
-import org.asamk.signal.GroupNotFoundException;
-import org.asamk.signal.NotAGroupMemberException;
-import org.asamk.signal.TrustLevel;
-import org.asamk.signal.UserAlreadyExists;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import org.asamk.signal.storage.SignalAccount;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.JsonGroupStore;
+import org.asamk.signal.storage.profiles.SignalProfile;
+import org.asamk.signal.storage.profiles.SignalProfileEntry;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
-import org.asamk.signal.storage.threads.ThreadInfo;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
@@ -41,6 +38,10 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException;
+import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.profiles.ClientZkProfileOperations;
+import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -59,18 +60,26 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
+import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
+import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
+import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
+import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload;
+import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload.StickerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
@@ -84,20 +93,27 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
+import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
+import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
+import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
+import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
+import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.Base64;
+import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -106,6 +122,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
@@ -118,49 +136,66 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
-public class Manager implements Signal {
+import static org.asamk.signal.manager.ServiceConfig.capabilities;
+
+public class Manager implements Closeable {
- private final String settingsPath;
- private final String dataPath;
- private final String attachmentsPath;
- private final String avatarsPath;
private final SleepTimer timer = new UptimeSleepTimer();
+ private final SignalServiceConfiguration serviceConfiguration;
+ private final String userAgent;
- private SignalAccount account;
- private String username;
+ private final SignalAccount account;
+ private final PathConfig pathConfig;
private SignalServiceAccountManager accountManager;
private SignalServiceMessagePipe messagePipe = null;
private SignalServiceMessagePipe unidentifiedMessagePipe = null;
+ private boolean discoverableByPhoneNumber = true;
- public Manager(String username, String settingsPath) {
- this.username = username;
- this.settingsPath = settingsPath;
- this.dataPath = this.settingsPath + "/data";
- this.attachmentsPath = this.settingsPath + "/attachments";
- this.avatarsPath = this.settingsPath + "/avatars";
+ public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) {
+ this.account = account;
+ this.pathConfig = pathConfig;
+ this.serviceConfiguration = serviceConfiguration;
+ this.userAgent = userAgent;
+ this.accountManager = createSignalServiceAccountManager();
+ this.account.setResolver(this::resolveSignalServiceAddress);
}
public String getUsername() {
- return username;
+ return account.getUsername();
}
- private SignalServiceAddress getSelfAddress() {
- return new SignalServiceAddress(null, username);
+ public SignalServiceAddress getSelfAddress() {
+ return account.getSelfAddress();
}
- private SignalServiceAccountManager getSignalServiceAccountManager() {
- return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer);
+ private SignalServiceAccountManager createSignalServiceAccountManager() {
+ GroupsV2Operations groupsV2Operations;
+ try {
+ groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration));
+ } catch (Throwable ignored) {
+ groupsV2Operations = null;
+ }
+ return new SignalServiceAccountManager(serviceConfiguration,
+ new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), null, account.getDeviceId()),
+ userAgent,
+ groupsV2Operations,
+ timer);
}
- private IdentityKey getIdentity() {
- return account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
+ private IdentityKeyPair getIdentityKeyPair() {
+ return account.getSignalProtocolStore().getIdentityKeyPair();
}
public int getDeviceId() {
@@ -168,10 +203,14 @@ public class Manager implements Signal {
}
private String getMessageCachePath() {
- return this.dataPath + "/" + username + ".d/msg-cache";
+ return pathConfig.getDataPath() + "/" + account.getUsername() + ".d/msg-cache";
}
private String getMessageCachePath(String sender) {
+ if (sender == null || sender.isEmpty()) {
+ return getMessageCachePath();
+ }
+
return getMessageCachePath() + "/" + sender.replace("/", "_");
}
@@ -181,28 +220,27 @@ public class Manager implements Signal {
return new File(cachePath + "/" + now + "_" + timestamp);
}
- public boolean userHasKeys() {
- return account != null && account.getSignalProtocolStore() != null;
- }
+ public static Manager init(String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) throws IOException {
+ PathConfig pathConfig = PathConfig.createDefault(settingsPath);
- public void init() throws IOException {
- if (!SignalAccount.userExists(dataPath, username)) {
- return;
+ if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
+ IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
+ int registrationId = KeyHelper.generateRegistrationId(false);
+
+ ProfileKey profileKey = KeyUtils.createProfileKey();
+ SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), username, identityKey, registrationId, profileKey);
+ account.save();
+
+ return new Manager(account, pathConfig, serviceConfiguration, userAgent);
}
- account = SignalAccount.load(dataPath, username);
- migrateLegacyConfigs();
+ SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username);
- accountManager = getSignalServiceAccountManager();
- try {
- if (account.isRegistered() && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) {
- refreshPreKeys();
- account.save();
- }
- } catch (AuthorizationFailedException e) {
- System.err.println("Authorization failed, was the number registered elsewhere?");
- throw e;
- }
+ Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent);
+
+ m.migrateLegacyConfigs();
+
+ return m;
}
private void migrateLegacyConfigs() {
@@ -211,10 +249,10 @@ public class Manager implements Signal {
if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) {
for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) {
File avatarFile = getGroupAvatarFile(g.groupId);
- File attachmentFile = getAttachmentFile(g.getAvatarId());
+ File attachmentFile = getAttachmentFile(new SignalServiceAttachmentRemoteId(g.getAvatarId()));
if (!avatarFile.exists() && attachmentFile.exists()) {
try {
- IOUtils.createPrivateDirectories(avatarsPath);
+ IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
// Ignore
@@ -231,33 +269,34 @@ public class Manager implements Signal {
}
}
- private void createNewIdentity() throws IOException {
- IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
- int registrationId = KeyHelper.generateRegistrationId(false);
- if (username == null) {
- account = SignalAccount.createTemporaryAccount(identityKey, registrationId);
- } else {
- byte[] profileKey = KeyUtils.createProfileKey();
- account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey);
- account.save();
+ public void checkAccountState() throws IOException {
+ if (account.isRegistered()) {
+ if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) {
+ refreshPreKeys();
+ account.save();
+ }
+ if (account.getUuid() == null) {
+ account.setUuid(accountManager.getOwnUuid());
+ account.save();
+ }
}
}
public boolean isRegistered() {
- return account != null && account.isRegistered();
+ return account.isRegistered();
}
public void register(boolean voiceVerification) throws IOException {
- if (account == null) {
- createNewIdentity();
- }
account.setPassword(KeyUtils.createPassword());
- accountManager = getSignalServiceAccountManager();
+
+ // Resetting UUID, because registering doesn't work otherwise
+ account.setUuid(null);
+ accountManager = createSignalServiceAccountManager();
if (voiceVerification) {
- accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent());
+ accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent());
} else {
- accountManager.requestSmsVerificationCode(false, Optional.absent(), Optional.absent());
+ accountManager.requestSmsVerificationCode(false, Optional.absent(), Optional.absent());
}
account.setRegistered(false);
@@ -265,71 +304,25 @@ public class Manager implements Signal {
}
public void updateAccountAttributes() throws IOException {
- accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), getSelfUnidentifiedAccessKey(), false);
+ accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
}
- public void setProfileName(String name) throws IOException {
- accountManager.setProfileName(account.getProfileKey(), name);
- }
-
- public void setProfileAvatar(File avatar) throws IOException {
- final StreamDetails streamDetails = Utils.createStreamDetailsFromFile(avatar);
- accountManager.setProfileAvatar(account.getProfileKey(), streamDetails);
- streamDetails.getStream().close();
- }
-
- public void removeProfileAvatar() throws IOException {
- accountManager.setProfileAvatar(account.getProfileKey(), null);
+ public void setProfile(String name, File avatar) throws IOException {
+ try (final StreamDetails streamDetails = avatar == null ? null : Utils.createStreamDetailsFromFile(avatar)) {
+ accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), name, streamDetails);
+ }
}
public void unregister() throws IOException {
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
// If this is the master device, other users can't send messages to this number anymore.
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
- accountManager.setGcmId(Optional.absent());
+ accountManager.setGcmId(Optional.absent());
account.setRegistered(false);
account.save();
}
- public String getDeviceLinkUri() throws TimeoutException, IOException {
- if (account == null) {
- createNewIdentity();
- }
- account.setPassword(KeyUtils.createPassword());
- accountManager = getSignalServiceAccountManager();
- String uuid = accountManager.getNewDeviceUuid();
-
- return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(uuid, getIdentity().getPublicKey()));
- }
-
- public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
- account.setSignalingKey(KeyUtils.createSignalingKey());
- SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(account.getSignalProtocolStore().getIdentityKeyPair(), account.getSignalingKey(), false, true, account.getSignalProtocolStore().getLocalRegistrationId(), deviceName);
-
- username = ret.getNumber();
- // TODO do this check before actually registering
- if (SignalAccount.userExists(dataPath, username)) {
- throw new UserAlreadyExists(username, SignalAccount.getFileName(dataPath, username));
- }
-
- // Create new account with the synced identity
- byte[] profileKey = ret.getProfileKey();
- if (profileKey == null) {
- profileKey = KeyUtils.createProfileKey();
- }
- account = SignalAccount.createLinkedAccount(dataPath, username, account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey);
-
- refreshPreKeys();
-
- requestSyncGroups();
- requestSyncContacts();
- requestSyncBlocked();
- requestSyncConfiguration();
-
- account.save();
- }
-
public List getLinkedDevices() throws IOException {
List devices = accountManager.getDevices();
account.setMultiDevice(devices.size() > 1);
@@ -351,19 +344,19 @@ public class Manager implements Signal {
}
private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
- IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair();
+ IdentityKeyPair identityKeyPair = getIdentityKeyPair();
String verificationCode = accountManager.getNewDeviceVerificationCode();
- accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey()), verificationCode);
+ accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey().serialize()), verificationCode);
account.setMultiDevice(true);
account.save();
}
private List generatePreKeys() {
- List records = new ArrayList<>(BaseConfig.PREKEY_BATCH_SIZE);
+ List records = new ArrayList<>(ServiceConfig.PREKEY_BATCH_SIZE);
final int offset = account.getPreKeyIdOffset();
- for (int i = 0; i < BaseConfig.PREKEY_BATCH_SIZE; i++) {
+ for (int i = 0; i < ServiceConfig.PREKEY_BATCH_SIZE; i++) {
int preKeyId = (offset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
@@ -396,41 +389,107 @@ public class Manager implements Signal {
verificationCode = verificationCode.replace("-", "");
account.setSignalingKey(KeyUtils.createSignalingKey());
// TODO make unrestricted unidentified access configurable
- accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false);
+ VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
+ UUID uuid = UuidUtil.parseOrNull(response.getUuid());
+ // TODO response.isStorageCapable()
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
account.setRegistered(true);
+ account.setUuid(uuid);
account.setRegistrationLockPin(pin);
+ account.getSignalProtocolStore().saveIdentity(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel.TRUSTED_VERIFIED);
refreshPreKeys();
account.save();
}
public void setRegistrationLockPin(Optional pin) throws IOException {
- accountManager.setPin(pin);
if (pin.isPresent()) {
account.setRegistrationLockPin(pin.get());
+ throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
} else {
account.setRegistrationLockPin(null);
+ accountManager.removeRegistrationLockV1();
}
account.save();
}
- private void refreshPreKeys() throws IOException {
+ void refreshPreKeys() throws IOException {
List oneTimePreKeys = generatePreKeys();
- final IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair();
+ final IdentityKeyPair identityKeyPair = getIdentityKeyPair();
SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(identityKeyPair);
- accountManager.setPreKeys(getIdentity(), signedPreKeyRecord, oneTimePreKeys);
+ accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
}
private SignalServiceMessageReceiver getMessageReceiver() {
- return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, null, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer);
+ // TODO implement ZkGroup support
+ final ClientZkProfileOperations clientZkProfileOperations = null;
+ return new SignalServiceMessageReceiver(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), userAgent, null, timer, clientZkProfileOperations);
}
private SignalServiceMessageSender getMessageSender() {
- return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, null, username, account.getPassword(),
- account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent());
+ // TODO implement ZkGroup support
+ final ClientZkProfileOperations clientZkProfileOperations = null;
+ final boolean attachmentsV3 = false;
+ final ExecutorService executor = null;
+ return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(),
+ account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations, executor);
+ }
+
+ private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException {
+ SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe
+ : messagePipe;
+
+ if (pipe != null) {
+ try {
+ return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
+ } catch (IOException | InterruptedException | ExecutionException | TimeoutException ignored) {
+ }
+ }
+
+ SignalServiceMessageReceiver receiver = getMessageReceiver();
+ try {
+ return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new IOException("Failed to retrieve profile", e);
+ }
+ }
+
+ private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException {
+ SignalProfileEntry profileEntry = account.getProfileStore().getProfile(address);
+ long now = new Date().getTime();
+ // Profiles are cache for 24h before retrieving them again
+ if (profileEntry == null || profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) {
+ SignalProfile profile = retrieveRecipientProfile(address, unidentifiedAccess, profileKey);
+ account.getProfileStore().updateProfile(address, profileKey, now, profile);
+ return profile;
+ }
+ return profileEntry.getProfile();
+ }
+
+ private SignalProfile retrieveRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException {
+ final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess);
+
+ File avatarFile = null;
+ try {
+ avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
+ } catch (Throwable e) {
+ System.err.println("Failed to retrieve profile avatar, ignoring: " + e.getMessage());
+ }
+
+ ProfileCipher profileCipher = new ProfileCipher(profileKey);
+ try {
+ return new SignalProfile(
+ encryptedProfile.getIdentityKey(),
+ encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
+ avatarFile,
+ encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
+ encryptedProfile.isUnrestrictedUnidentifiedAccess(),
+ encryptedProfile.getCapabilities());
+ } catch (InvalidCiphertextException e) {
+ return null;
+ }
}
private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException {
@@ -456,22 +515,19 @@ public class Manager implements Signal {
if (g == null) {
throw new GroupNotFoundException(groupId);
}
- for (String member : g.members) {
- if (member.equals(this.username)) {
- return g;
- }
+ if (!g.isMember(account.getSelfAddress())) {
+ throw new NotAGroupMemberException(groupId, g.name);
}
- throw new NotAGroupMemberException(groupId, g.name);
+ return g;
}
public List getGroups() {
return account.getGroupStore().getGroups();
}
- @Override
- public void sendGroupMessage(String messageText, List attachments,
+ public long sendGroupMessage(String messageText, List attachments,
byte[] groupId)
- throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+ throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
if (attachments != null) {
messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments));
@@ -482,20 +538,31 @@ public class Manager implements Signal {
.build();
messageBuilder.asGroupMessage(group);
}
- ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId));
- if (thread != null) {
- messageBuilder.withExpiration(thread.messageExpirationTime);
- }
final GroupInfo g = getGroupForSending(groupId);
- // Don't send group message to ourself
- final List membersSend = new ArrayList<>(g.members);
- membersSend.remove(this.username);
- sendMessageLegacy(messageBuilder, membersSend);
+ messageBuilder.withExpiration(g.messageExpirationTime);
+
+ return sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
+ public void sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor,
+ long targetSentTimestamp, byte[] groupId)
+ throws IOException, EncapsulatedExceptions, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException {
+ SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp);
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .withReaction(reaction);
+ if (groupId != null) {
+ SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
+ .withId(groupId)
+ .build();
+ messageBuilder.asGroupMessage(group);
+ }
+ final GroupInfo g = getGroupForSending(groupId);
+ sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+ }
+
+ public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, NotAGroupMemberException {
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
.withId(groupId)
.build();
@@ -504,18 +571,18 @@ public class Manager implements Signal {
.asGroupMessage(group);
final GroupInfo g = getGroupForSending(groupId);
- g.members.remove(this.username);
+ g.removeMember(account.getSelfAddress());
account.getGroupStore().updateGroup(g);
- sendMessageLegacy(messageBuilder, g.members);
+ sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+ private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
GroupInfo g;
if (groupId == null) {
// Create new group
g = new GroupInfo(KeyUtils.createGroupId());
- g.members.add(username);
+ g.addMembers(Collections.singleton(account.getSelfAddress()));
} else {
g = getGroupForSending(groupId);
}
@@ -525,35 +592,28 @@ public class Manager implements Signal {
}
if (members != null) {
- Set newMembers = new HashSet<>();
- for (String member : members) {
- try {
- member = Utils.canonicalizeNumber(member, username);
- } catch (InvalidNumberException e) {
- System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
- System.err.println("Aborting…");
- System.exit(1);
- }
- if (g.members.contains(member)) {
+ final Set newE164Members = new HashSet<>();
+ for (SignalServiceAddress member : members) {
+ if (g.isMember(member) || !member.getNumber().isPresent()) {
continue;
}
- newMembers.add(member);
- g.members.add(member);
+ newE164Members.add(member.getNumber().get());
}
- final List contacts = accountManager.getContacts(newMembers);
- if (contacts.size() != newMembers.size()) {
+
+ final List contacts = accountManager.getContacts(newE164Members);
+ if (contacts.size() != newE164Members.size()) {
// Some of the new members are not registered on Signal
for (ContactTokenDetails contact : contacts) {
- newMembers.remove(contact.getNumber());
+ newE164Members.remove(contact.getNumber());
}
- System.err.println("Failed to add members " + Util.join(", ", newMembers) + " to group: Not registered on Signal");
- System.err.println("Aborting…");
- System.exit(1);
+ throw new IOException("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal");
}
+
+ g.addMembers(members);
}
if (avatarFile != null) {
- IOUtils.createPrivateDirectories(avatarsPath);
+ IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
File aFile = getGroupAvatarFile(g.groupId);
Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
@@ -562,32 +622,27 @@ public class Manager implements Signal {
SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
- // Don't send group message to ourself
- final List membersSend = new ArrayList<>(g.members);
- membersSend.remove(this.username);
- sendMessageLegacy(messageBuilder, membersSend);
+ sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
return g.groupId;
}
- private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
+ void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
if (groupId == null) {
return;
}
GroupInfo g = getGroupForSending(groupId);
- if (!g.members.contains(recipient)) {
+ if (!g.isMember(recipient)) {
return;
}
SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
// Send group message only to the recipient who requested it
- final List membersSend = new ArrayList<>();
- membersSend.add(recipient);
- sendMessageLegacy(messageBuilder, membersSend);
+ sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
}
- private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
+ private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) throws AttachmentInvalidException {
SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
.withId(g.groupId)
.withName(g.name)
@@ -602,18 +657,12 @@ public class Manager implements Signal {
}
}
- SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
- .asGroupMessage(group.build());
-
- ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(g.groupId));
- if (thread != null) {
- messageBuilder.withExpiration(thread.messageExpirationTime);
- }
-
- return messageBuilder;
+ return SignalServiceDataMessage.newBuilder()
+ .asGroupMessage(group.build())
+ .withExpiration(g.messageExpirationTime);
}
- private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
+ void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
if (groupId == null) {
return;
}
@@ -624,29 +673,21 @@ public class Manager implements Signal {
SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build());
- ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId));
- if (thread != null) {
- messageBuilder.withExpiration(thread.messageExpirationTime);
- }
-
// Send group info request message to the recipient who sent us a message with this groupId
- final List membersSend = new ArrayList<>();
- membersSend.add(recipient);
- sendMessageLegacy(messageBuilder, membersSend);
+ sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
}
- @Override
- public void sendMessage(String message, List attachments, String recipient)
- throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
- List recipients = new ArrayList<>(1);
- recipients.add(recipient);
- sendMessage(message, attachments, recipients);
+ void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException {
+ SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
+ Collections.singletonList(messageId),
+ System.currentTimeMillis());
+
+ getMessageSender().sendReceipt(remoteAddress, getAccessFor(remoteAddress), receiptMessage);
}
- @Override
- public void sendMessage(String messageText, List attachments,
+ public long sendMessage(String messageText, List attachments,
List recipients)
- throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
+ throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
if (attachments != null) {
List attachmentStreams = Utils.getSignalServiceAttachments(attachments);
@@ -664,22 +705,36 @@ public class Manager implements Signal {
messageBuilder.withAttachments(attachmentPointers);
}
- messageBuilder.withProfileKey(account.getProfileKey());
- sendMessageLegacy(messageBuilder, recipients);
+ return sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
}
- @Override
- public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions {
+ public void sendMessageReaction(String emoji, boolean remove, String targetAuthor,
+ long targetSentTimestamp, List recipients)
+ throws IOException, EncapsulatedExceptions, InvalidNumberException {
+ SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp);
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .withReaction(reaction);
+ sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
+ }
+
+ public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException {
SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asEndSessionMessage();
- sendMessageLegacy(messageBuilder, recipients);
+ final Collection signalServiceAddresses = getSignalServiceAddresses(recipients);
+ try {
+ sendMessageLegacy(messageBuilder, signalServiceAddresses);
+ } catch (Exception e) {
+ for (SignalServiceAddress address : signalServiceAddresses) {
+ handleEndSession(address);
+ }
+ account.save();
+ throw e;
+ }
}
- @Override
public String getContactName(String number) throws InvalidNumberException {
- String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
- ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber);
+ ContactInfo contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number));
if (contact == null) {
return "";
} else {
@@ -687,111 +742,200 @@ public class Manager implements Signal {
}
}
- @Override
public void setContactName(String number, String name) throws InvalidNumberException {
- String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
- ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber);
+ final SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number);
+ ContactInfo contact = account.getContactStore().getContact(address);
if (contact == null) {
- contact = new ContactInfo();
- contact.number = canonicalizedNumber;
- System.err.println("Add contact " + canonicalizedNumber + " named " + name);
- } else {
- System.err.println("Updating contact " + canonicalizedNumber + " name " + contact.name + " -> " + name);
+ contact = new ContactInfo(address);
}
contact.name = name;
account.getContactStore().updateContact(contact);
account.save();
}
- @Override
public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
- number = Utils.canonicalizeNumber(number, username);
- ContactInfo contact = account.getContactStore().getContact(number);
+ setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked);
+ }
+
+ private void setContactBlocked(SignalServiceAddress address, boolean blocked) {
+ ContactInfo contact = account.getContactStore().getContact(address);
if (contact == null) {
- contact = new ContactInfo();
- contact.number = number;
- System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number);
- } else {
- System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + number);
+ contact = new ContactInfo(address);
}
contact.blocked = blocked;
account.getContactStore().updateContact(contact);
account.save();
}
- @Override
public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException {
GroupInfo group = getGroup(groupId);
if (group == null) {
throw new GroupNotFoundException(groupId);
- } else {
- System.err.println((blocked ? "Blocking" : "Unblocking") + " group " + Base64.encodeBytes(groupId));
- group.blocked = blocked;
- account.getGroupStore().updateGroup(group);
- account.save();
}
+
+ group.blocked = blocked;
+ account.getGroupStore().updateGroup(group);
+ account.save();
}
- @Override
- public List getGroupIds() {
- List groups = getGroups();
- List ids = new ArrayList<>(groups.size());
- for (GroupInfo group : groups) {
- ids.add(group.groupId);
- }
- return ids;
- }
-
- @Override
- public String getGroupName(byte[] groupId) {
- GroupInfo group = getGroup(groupId);
- if (group == null) {
- return "";
- } else {
- return group.name;
- }
- }
-
- @Override
- public List getGroupMembers(byte[] groupId) {
- GroupInfo group = getGroup(groupId);
- if (group == null) {
- return new ArrayList<>();
- } else {
- return new ArrayList<>(group.members);
- }
- }
-
- @Override
- public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+ public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
if (groupId.length == 0) {
groupId = null;
}
if (name.isEmpty()) {
name = null;
}
- if (members.size() == 0) {
+ if (members.isEmpty()) {
members = null;
}
if (avatar.isEmpty()) {
avatar = null;
}
- return sendUpdateGroupMessage(groupId, name, members, avatar);
+ return sendUpdateGroupMessage(groupId, name, members == null ? null : getSignalServiceAddresses(members), avatar);
}
/**
- * Change the expiration timer for a thread (number of groupId)
- *
- * @param numberOrGroupId
- * @param messageExpirationTimer
+ * Change the expiration timer for a contact
*/
- public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) {
- ThreadInfo thread = account.getThreadStore().getThread(numberOrGroupId);
- thread.messageExpirationTime = messageExpirationTimer;
- account.getThreadStore().updateThread(thread);
+ public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException {
+ ContactInfo contact = account.getContactStore().getContact(address);
+ contact.messageExpirationTime = messageExpirationTimer;
+ account.getContactStore().updateContact(contact);
+ sendExpirationTimerUpdate(address);
+ account.save();
}
- private void requestSyncGroups() throws IOException {
+ private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .asExpirationUpdate();
+ sendMessage(messageBuilder, Collections.singleton(address));
+ }
+
+ /**
+ * Change the expiration timer for a contact
+ */
+ public void setExpirationTimer(String number, int messageExpirationTimer) throws IOException, InvalidNumberException {
+ SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number);
+ setExpirationTimer(address, messageExpirationTimer);
+ }
+
+ /**
+ * Change the expiration timer for a group
+ */
+ public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) {
+ GroupInfo g = account.getGroupStore().getGroup(groupId);
+ g.messageExpirationTime = messageExpirationTimer;
+ account.getGroupStore().updateGroup(g);
+ }
+
+ /**
+ * Upload the sticker pack from path.
+ *
+ * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
+ * @return if successful, returns the URL to install the sticker pack in the signal app
+ */
+ public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException {
+ SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path);
+
+ SignalServiceMessageSender messageSender = getMessageSender();
+
+ byte[] packKey = KeyUtils.createStickerUploadKey();
+ String packId = messageSender.uploadStickerManifest(manifest, packKey);
+
+ try {
+ return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, "utf-8") + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), "utf-8"))
+ .toString();
+ } catch (URISyntaxException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(final String path) throws IOException, StickerPackInvalidException {
+ ZipFile zip = null;
+ String rootPath = null;
+
+ final File file = new File(path);
+ if (file.getName().endsWith(".zip")) {
+ zip = new ZipFile(file);
+ } else if (file.getName().equals("manifest.json")) {
+ rootPath = file.getParent();
+ } else {
+ throw new StickerPackInvalidException("Could not find manifest.json");
+ }
+
+ JsonStickerPack pack = parseStickerPack(rootPath, zip);
+
+ if (pack.stickers == null) {
+ throw new StickerPackInvalidException("Must set a 'stickers' field.");
+ }
+
+ if (pack.stickers.isEmpty()) {
+ throw new StickerPackInvalidException("Must include stickers.");
+ }
+
+ List stickers = new ArrayList<>(pack.stickers.size());
+ for (JsonStickerPack.JsonSticker sticker : pack.stickers) {
+ if (sticker.file == null) {
+ throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
+ }
+
+ Pair data;
+ try {
+ data = getInputStreamAndLength(rootPath, zip, sticker.file);
+ } catch (IOException ignored) {
+ throw new StickerPackInvalidException("Could not find find " + sticker.file);
+ }
+
+ String contentType = Utils.getFileMimeType(new File(sticker.file), null);
+ StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""), contentType);
+ stickers.add(stickerInfo);
+ }
+
+ StickerInfo cover = null;
+ if (pack.cover != null) {
+ if (pack.cover.file == null) {
+ throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
+ }
+
+ Pair data;
+ try {
+ data = getInputStreamAndLength(rootPath, zip, pack.cover.file);
+ } catch (IOException ignored) {
+ throw new StickerPackInvalidException("Could not find find " + pack.cover.file);
+ }
+
+ String contentType = Utils.getFileMimeType(new File(pack.cover.file), null);
+ cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""), contentType);
+ }
+
+ return new SignalServiceStickerManifestUpload(
+ pack.title,
+ pack.author,
+ cover,
+ stickers);
+ }
+
+ private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException {
+ InputStream inputStream;
+ if (zip != null) {
+ inputStream = zip.getInputStream(zip.getEntry("manifest.json"));
+ } else {
+ inputStream = new FileInputStream((new File(rootPath, "manifest.json")));
+ }
+ return new ObjectMapper().readValue(inputStream, JsonStickerPack.class);
+ }
+
+ private static Pair getInputStreamAndLength(final String rootPath, final ZipFile zip, final String subfile) throws IOException {
+ if (zip != null) {
+ final ZipEntry entry = zip.getEntry(subfile);
+ return new Pair<>(zip.getInputStream(entry), entry.getSize());
+ } else {
+ final File file = new File(rootPath, subfile);
+ return new Pair<>(new FileInputStream(file), file.length());
+ }
+ }
+
+ void requestSyncGroups() throws IOException {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
@@ -801,7 +945,7 @@ public class Manager implements Signal {
}
}
- private void requestSyncContacts() throws IOException {
+ void requestSyncContacts() throws IOException {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
@@ -811,7 +955,7 @@ public class Manager implements Signal {
}
}
- private void requestSyncBlocked() throws IOException {
+ void requestSyncBlocked() throws IOException {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED).build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
@@ -821,7 +965,7 @@ public class Manager implements Signal {
}
}
- private void requestSyncConfiguration() throws IOException {
+ void requestSyncConfiguration() throws IOException {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION).build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
@@ -831,30 +975,106 @@ public class Manager implements Signal {
}
}
+ private byte[] getSenderCertificate() {
+ // TODO support UUID capable sender certificates
+ // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
+ byte[] certificate;
+ try {
+ certificate = accountManager.getSenderCertificate();
+ } catch (IOException e) {
+ System.err.println("Failed to get sender certificate: " + e);
+ return null;
+ }
+ // TODO cache for a day
+ return certificate;
+ }
+
private byte[] getSelfUnidentifiedAccessKey() {
return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
}
private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
- // TODO implement
- return null;
+ ContactInfo contact = account.getContactStore().getContact(recipient);
+ if (contact == null || contact.profileKey == null) {
+ return null;
+ }
+ ProfileKey theirProfileKey;
+ try {
+ theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey));
+ } catch (InvalidInputException | IOException e) {
+ throw new AssertionError(e);
+ }
+ SignalProfile targetProfile;
+ try {
+ targetProfile = getRecipientProfile(recipient, Optional.absent(), theirProfileKey);
+ } catch (IOException e) {
+ System.err.println("Failed to get recipient profile: " + e);
+ return null;
+ }
+
+ if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
+ return null;
+ }
+
+ if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
+ return KeyUtils.createUnrestrictedUnidentifiedAccess();
+ }
+
+ return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
}
private Optional getAccessForSync() {
- // TODO implement
- return Optional.absent();
+ byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
+ byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
+
+ if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
+ return Optional.absent();
+ }
+
+ try {
+ return Optional.of(new UnidentifiedAccessPair(
+ new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
+ new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
+ ));
+ } catch (InvalidCertificateException e) {
+ return Optional.absent();
+ }
}
private List> getAccessFor(Collection recipients) {
List> result = new ArrayList<>(recipients.size());
for (SignalServiceAddress recipient : recipients) {
- result.add(Optional.absent());
+ result.add(getAccessFor(recipient));
}
return result;
}
private Optional getAccessFor(SignalServiceAddress recipient) {
- // TODO implement
+ byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
+ byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
+ byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
+
+ if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
+ return Optional.absent();
+ }
+
+ try {
+ return Optional.of(new UnidentifiedAccessPair(
+ new UnidentifiedAccess(recipientUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
+ new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
+ ));
+ } catch (InvalidCertificateException e) {
+ return Optional.absent();
+ }
+ }
+
+ private Optional getUnidentifiedAccess(SignalServiceAddress recipient) {
+ Optional unidentifiedAccess = getAccessFor(recipient);
+
+ if (unidentifiedAccess.isPresent()) {
+ return unidentifiedAccess.get().getTargetUnidentifiedAccess();
+ }
+
return Optional.absent();
}
@@ -864,7 +1084,7 @@ public class Manager implements Signal {
try {
messageSender.sendMessage(message, getAccessForSync());
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
throw e;
}
}
@@ -872,8 +1092,10 @@ public class Manager implements Signal {
/**
* This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
*/
- private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients)
+ private long sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients)
throws EncapsulatedExceptions, IOException {
+ final long timestamp = System.currentTimeMillis();
+ messageBuilder.withTimestamp(timestamp);
List results = sendMessage(messageBuilder, recipients);
List untrustedIdentities = new LinkedList<>();
@@ -882,115 +1104,141 @@ public class Manager implements Signal {
for (SendMessageResult result : results) {
if (result.isUnregisteredFailure()) {
- unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getNumber().get(), null));
+ unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getLegacyIdentifier(), null));
} else if (result.isNetworkFailure()) {
- networkExceptions.add(new NetworkFailureException(result.getAddress().getNumber().get(), null));
+ networkExceptions.add(new NetworkFailureException(result.getAddress().getLegacyIdentifier(), null));
} else if (result.getIdentityFailure() != null) {
- untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getNumber().get(), result.getIdentityFailure().getIdentityKey()));
+ untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getLegacyIdentifier(), result.getIdentityFailure().getIdentityKey()));
}
}
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
}
+ return timestamp;
}
- private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients)
- throws IOException {
- Set recipientsTS = Utils.getSignalServiceAddresses(recipients, username);
- if (recipientsTS == null) {
- account.save();
- return Collections.emptyList();
- }
+ private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException {
+ final Set signalServiceAddresses = new HashSet<>(numbers.size());
+ for (String number : numbers) {
+ signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number));
+ }
+ return signalServiceAddresses;
+ }
+
+ private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients)
+ throws IOException {
+ if (messagePipe == null) {
+ messagePipe = getMessageReceiver().createMessagePipe();
+ }
+ if (unidentifiedMessagePipe == null) {
+ unidentifiedMessagePipe = getMessageReceiver().createUnidentifiedMessagePipe();
+ }
SignalServiceDataMessage message = null;
try {
- SignalServiceMessageSender messageSender = getMessageSender();
-
message = messageBuilder.build();
- if (message.getGroupInfo().isPresent()) {
+ if (message.getGroupContext().isPresent()) {
try {
+ SignalServiceMessageSender messageSender = getMessageSender();
final boolean isRecipientUpdate = false;
- List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), isRecipientUpdate, message);
+ List result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message);
for (SendMessageResult r : result) {
if (r.getIdentityFailure() != null) {
- account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber().get(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore().saveIdentity(r.getAddress(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
}
}
return result;
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
return Collections.emptyList();
}
- } else if (recipientsTS.size() == 1 && recipientsTS.contains(getSelfAddress())) {
- SignalServiceAddress recipient = getSelfAddress();
- final Optional unidentifiedAccess = getAccessFor(recipient);
- SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient),
- message.getTimestamp(),
- message,
- message.getExpiresInSeconds(),
- Collections.singletonMap(recipient, unidentifiedAccess.isPresent()),
- false);
- SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
-
- List results = new ArrayList<>(recipientsTS.size());
- try {
- messageSender.sendMessage(syncMessage, unidentifiedAccess);
- } catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
- results.add(SendMessageResult.identityFailure(recipient, e.getIdentityKey()));
- }
- return results;
} else {
// Send to all individually, so sync messages are sent correctly
- List results = new ArrayList<>(recipientsTS.size());
- for (SignalServiceAddress address : recipientsTS) {
- ThreadInfo thread = account.getThreadStore().getThread(address.getNumber().get());
- if (thread != null) {
- messageBuilder.withExpiration(thread.messageExpirationTime);
+ List results = new ArrayList<>(recipients.size());
+ for (SignalServiceAddress address : recipients) {
+ ContactInfo contact = account.getContactStore().getContact(address);
+ if (contact != null) {
+ messageBuilder.withExpiration(contact.messageExpirationTime);
+ messageBuilder.withProfileKey(account.getProfileKey().serialize());
} else {
messageBuilder.withExpiration(0);
+ messageBuilder.withProfileKey(null);
}
message = messageBuilder.build();
- try {
- SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message);
- results.add(result);
- } catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
- results.add(SendMessageResult.identityFailure(address, e.getIdentityKey()));
+ if (address.matches(account.getSelfAddress())) {
+ results.add(sendSelfMessage(message));
+ } else {
+ results.add(sendMessage(address, message));
}
}
return results;
}
} finally {
if (message != null && message.isEndSession()) {
- for (SignalServiceAddress recipient : recipientsTS) {
- handleEndSession(recipient.getNumber().get());
+ for (SignalServiceAddress recipient : recipients) {
+ handleEndSession(recipient);
}
}
account.save();
}
}
- private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException, UnsupportedDataMessageException {
- SignalServiceCipher cipher = new SignalServiceCipher(getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator());
+ private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException {
+ SignalServiceMessageSender messageSender = getMessageSender();
+
+ SignalServiceAddress recipient = account.getSelfAddress();
+
+ final Optional unidentifiedAccess = getAccessFor(recipient);
+ SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient),
+ message.getTimestamp(),
+ message,
+ message.getExpiresInSeconds(),
+ Collections.singletonMap(recipient, unidentifiedAccess.isPresent()),
+ false);
+ SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
+
try {
- return cipher.decrypt(envelope);
- } catch (ProtocolUntrustedIdentityException e) {
- // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
-// account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
- throw e;
+ messageSender.sendMessage(syncMessage, unidentifiedAccess);
+ return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false);
+ } catch (UntrustedIdentityException e) {
+ account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
}
}
- private void handleEndSession(String source) {
+ private SendMessageResult sendMessage(SignalServiceAddress address, SignalServiceDataMessage message) throws IOException {
+ SignalServiceMessageSender messageSender = getMessageSender();
+
+ try {
+ return messageSender.sendMessage(address, getAccessFor(address), message);
+ } catch (UntrustedIdentityException e) {
+ account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ return SendMessageResult.identityFailure(address, e.getIdentityKey());
+ }
+ }
+
+ private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException {
+ SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator());
+ try {
+ return cipher.decrypt(envelope);
+ } catch (ProtocolUntrustedIdentityException e) {
+ if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
+ org.whispersystems.libsignal.UntrustedIdentityException identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
+ account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException.getName()), identityException.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
+ throw identityException;
+ }
+ throw new AssertionError(e);
+ }
+ }
+
+ private void handleEndSession(SignalServiceAddress source) {
account.getSignalProtocolStore().deleteAllSessions(source);
}
- private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, SignalServiceAddress destination, boolean ignoreAttachments) {
- String threadId;
- if (message.getGroupInfo().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupInfo().get();
- threadId = Base64.encodeBytes(groupInfo.getGroupId());
+ private List handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) {
+ List actions = new ArrayList<>();
+ if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
switch (groupInfo.getType()) {
case UPDATE:
@@ -1003,8 +1251,8 @@ public class Manager implements Signal {
if (avatar.isPointer()) {
try {
retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
- } catch (IOException | InvalidMessageException e) {
- System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
+ } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
+ System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getRemoteId() + "): " + e.getMessage());
}
}
}
@@ -1014,63 +1262,56 @@ public class Manager implements Signal {
}
if (groupInfo.getMembers().isPresent()) {
- group.addMembers(groupInfo.getMembers().get());
+ group.addMembers(groupInfo.getMembers().get()
+ .stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toSet()));
}
account.getGroupStore().updateGroup(group);
break;
case DELIVER:
- if (group == null) {
- try {
- sendGroupInfoRequest(groupInfo.getGroupId(), source);
- } catch (IOException | EncapsulatedExceptions e) {
- e.printStackTrace();
- }
+ if (group == null && !isSync) {
+ actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId()));
}
break;
case QUIT:
- if (group == null) {
- try {
- sendGroupInfoRequest(groupInfo.getGroupId(), source);
- } catch (IOException | EncapsulatedExceptions e) {
- e.printStackTrace();
- }
- } else {
- group.members.remove(source);
+ if (group != null) {
+ group.removeMember(source);
account.getGroupStore().updateGroup(group);
}
break;
case REQUEST_INFO:
- if (group != null) {
- try {
- sendUpdateGroupMessage(groupInfo.getGroupId(), source);
- } catch (IOException | EncapsulatedExceptions e) {
- e.printStackTrace();
- } catch (NotAGroupMemberException e) {
- // We have left this group, so don't send a group update message
- }
+ if (group != null && !isSync) {
+ actions.add(new SendGroupUpdateAction(source, group.groupId));
}
break;
}
- } else {
- if (isSync) {
- threadId = destination.getNumber().get();
- } else {
- threadId = source;
- }
}
+ final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source;
if (message.isEndSession()) {
- handleEndSession(isSync ? destination.getNumber().get() : source);
+ handleEndSession(conversationPartnerAddress);
}
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
- ThreadInfo thread = account.getThreadStore().getThread(threadId);
- if (thread == null) {
- thread = new ThreadInfo();
- thread.id = threadId;
- }
- if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
- thread.messageExpirationTime = message.getExpiresInSeconds();
- account.getThreadStore().updateThread(thread);
+ if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
+ GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
+ if (group == null) {
+ group = new GroupInfo(groupInfo.getGroupId());
+ }
+ if (group.messageExpirationTime != message.getExpiresInSeconds()) {
+ group.messageExpirationTime = message.getExpiresInSeconds();
+ account.getGroupStore().updateGroup(group);
+ }
+ } else {
+ ContactInfo contact = account.getContactStore().getContact(conversationPartnerAddress);
+ if (contact == null) {
+ contact = new ContactInfo(conversationPartnerAddress);
+ }
+ if (contact.messageExpirationTime != message.getExpiresInSeconds()) {
+ contact.messageExpirationTime = message.getExpiresInSeconds();
+ account.getContactStore().updateContact(contact);
+ }
}
}
if (message.getAttachments().isPresent() && !ignoreAttachments) {
@@ -1078,22 +1319,31 @@ public class Manager implements Signal {
if (attachment.isPointer()) {
try {
retrieveAttachment(attachment.asPointer());
- } catch (IOException | InvalidMessageException e) {
- System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
+ } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
+ System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getRemoteId() + "): " + e.getMessage());
}
}
}
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
- if (source.equals(username)) {
- this.account.setProfileKey(message.getProfileKey().get());
+ if (source.matches(account.getSelfAddress())) {
+ try {
+ this.account.setProfileKey(new ProfileKey(message.getProfileKey().get()));
+ } catch (InvalidInputException ignored) {
+ }
+ ContactInfo contact = account.getContactStore().getContact(source);
+ if (contact != null) {
+ contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
+ account.getContactStore().updateContact(contact);
+ }
+ } else {
+ ContactInfo contact = account.getContactStore().getContact(source);
+ if (contact == null) {
+ contact = new ContactInfo(source);
+ }
+ contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
+ account.getContactStore().updateContact(contact);
}
- ContactInfo contact = account.getContactStore().getContact(source);
- if (contact == null) {
- contact = new ContactInfo();
- contact.number = source;
- }
- contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
}
if (message.getPreviews().isPresent()) {
final List previews = message.getPreviews().get();
@@ -1102,12 +1352,13 @@ public class Manager implements Signal {
SignalServiceAttachmentPointer attachment = preview.getImage().get().asPointer();
try {
retrieveAttachment(attachment);
- } catch (IOException | InvalidMessageException e) {
- System.err.println("Failed to retrieve attachment (" + attachment.getId() + "): " + e.getMessage());
+ } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
+ System.err.println("Failed to retrieve attachment (" + attachment.getRemoteId() + "): " + e.getMessage());
}
}
}
}
+ return actions;
}
private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
@@ -1117,6 +1368,7 @@ public class Manager implements Signal {
}
for (final File dir : Objects.requireNonNull(cachePath.listFiles())) {
if (!dir.isDirectory()) {
+ retryFailedReceivedMessage(handler, ignoreAttachments, dir);
continue;
}
@@ -1124,101 +1376,147 @@ public class Manager implements Signal {
if (!fileEntry.isFile()) {
continue;
}
- SignalServiceEnvelope envelope;
- try {
- envelope = Utils.loadEnvelope(fileEntry);
- if (envelope == null) {
- continue;
- }
- } catch (IOException e) {
- e.printStackTrace();
- continue;
- }
- SignalServiceContent content = null;
- if (!envelope.isReceipt()) {
- try {
- content = decryptMessage(envelope);
- } catch (Exception e) {
- continue;
- }
- handleMessage(envelope, content, ignoreAttachments);
- }
- account.save();
- handler.handleMessage(envelope, content, null);
- try {
- Files.delete(fileEntry.toPath());
- } catch (IOException e) {
- System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
- }
+ retryFailedReceivedMessage(handler, ignoreAttachments, fileEntry);
}
// Try to delete directory if empty
dir.delete();
}
}
+ private void retryFailedReceivedMessage(final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry) {
+ SignalServiceEnvelope envelope;
+ try {
+ envelope = Utils.loadEnvelope(fileEntry);
+ if (envelope == null) {
+ return;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ }
+ SignalServiceContent content = null;
+ if (!envelope.isReceipt()) {
+ try {
+ content = decryptMessage(envelope);
+ } catch (Exception e) {
+ return;
+ }
+ List actions = handleMessage(envelope, content, ignoreAttachments);
+ for (HandleAction action : actions) {
+ try {
+ action.execute(this);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ account.save();
+ handler.handleMessage(envelope, content, null);
+ try {
+ Files.delete(fileEntry.toPath());
+ } catch (IOException e) {
+ System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
+ }
+ }
+
public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
retryFailedReceivedMessages(handler, ignoreAttachments);
final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
- try {
- if (messagePipe == null) {
- messagePipe = messageReceiver.createMessagePipe();
- }
+ Set queuedActions = null;
- while (true) {
- SignalServiceEnvelope envelope;
- SignalServiceContent content = null;
- Exception exception = null;
- final long now = new Date().getTime();
- try {
- envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() {
- @Override
- public void onMessage(SignalServiceEnvelope envelope) {
- // store message on disk, before acknowledging receipt to the server
+ if (messagePipe == null) {
+ messagePipe = messageReceiver.createMessagePipe();
+ }
+
+ boolean hasCaughtUpWithOldMessages = false;
+
+ while (true) {
+ SignalServiceEnvelope envelope;
+ SignalServiceContent content = null;
+ Exception exception = null;
+ final long now = new Date().getTime();
+ try {
+ Optional result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> {
+ // store message on disk, before acknowledging receipt to the server
+ try {
+ String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : "";
+ File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp());
+ Utils.storeEnvelope(envelope1, cacheFile);
+ } catch (IOException e) {
+ System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
+ }
+ });
+ if (result.isPresent()) {
+ envelope = result.get();
+ } else {
+ // Received indicator that server queue is empty
+ hasCaughtUpWithOldMessages = true;
+
+ if (queuedActions != null) {
+ for (HandleAction action : queuedActions) {
try {
- File cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp());
- Utils.storeEnvelope(envelope, cacheFile);
- } catch (IOException e) {
- System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
+ action.execute(this);
+ } catch (Throwable e) {
+ e.printStackTrace();
}
}
- });
- } catch (TimeoutException e) {
- if (returnOnTimeout)
- return;
- continue;
- } catch (InvalidVersionException e) {
- System.err.println("Ignoring error: " + e.getMessage());
+ queuedActions.clear();
+ queuedActions = null;
+ }
+
+ // Continue to wait another timeout for new messages
continue;
}
- if (!envelope.isReceipt()) {
- try {
- content = decryptMessage(envelope);
- } catch (Exception e) {
- exception = e;
- }
- handleMessage(envelope, content, ignoreAttachments);
+ } catch (TimeoutException e) {
+ if (returnOnTimeout)
+ return;
+ continue;
+ } catch (InvalidVersionException e) {
+ System.err.println("Ignoring error: " + e.getMessage());
+ continue;
+ }
+ if (envelope.hasSource()) {
+ // Store uuid if we don't have it already
+ SignalServiceAddress source = envelope.getSourceAddress();
+ resolveSignalServiceAddress(source);
+ }
+ if (!envelope.isReceipt()) {
+ try {
+ content = decryptMessage(envelope);
+ } catch (Exception e) {
+ exception = e;
}
- account.save();
- if (!isMessageBlocked(envelope, content)) {
- handler.handleMessage(envelope, content, exception);
- }
- if (!(exception instanceof ProtocolUntrustedIdentityException)) {
- File cacheFile = null;
- try {
- cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp());
- Files.delete(cacheFile.toPath());
- // Try to delete directory if empty
- new File(getMessageCachePath()).delete();
- } catch (IOException e) {
- System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
+ List actions = handleMessage(envelope, content, ignoreAttachments);
+ if (hasCaughtUpWithOldMessages) {
+ for (HandleAction action : actions) {
+ try {
+ action.execute(this);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
}
+ } else {
+ if (queuedActions == null) {
+ queuedActions = new HashSet<>();
+ }
+ queuedActions.addAll(actions);
}
}
- } finally {
- if (messagePipe != null) {
- messagePipe.shutdown();
- messagePipe = null;
+ account.save();
+ if (!isMessageBlocked(envelope, content)) {
+ handler.handleMessage(envelope, content, exception);
+ }
+ if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
+ File cacheFile = null;
+ try {
+ cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp());
+ Files.delete(cacheFile.toPath());
+ // Try to delete directory if empty
+ new File(getMessageCachePath()).delete();
+ } catch (IOException e) {
+ System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
+ }
}
}
}
@@ -1232,15 +1530,15 @@ public class Manager implements Signal {
} else {
return false;
}
- ContactInfo sourceContact = getContact(source.getNumber().get());
+ ContactInfo sourceContact = account.getContactStore().getContact(source);
if (sourceContact != null && sourceContact.blocked) {
return true;
}
if (content != null && content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
- if (message.getGroupInfo().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupInfo().get();
+ if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
GroupInfo group = getGroup(groupInfo.getGroupId());
if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER && group != null && group.blocked) {
return true;
@@ -1250,7 +1548,8 @@ public class Manager implements Signal {
return false;
}
- private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
+ private List handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
+ List actions = new ArrayList<>();
if (content != null) {
SignalServiceAddress sender;
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
@@ -1258,39 +1557,35 @@ public class Manager implements Signal {
} else {
sender = content.getSender();
}
+ // Store uuid if we don't have it already
+ resolveSignalServiceAddress(sender);
+
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
- handleSignalServiceDataMessage(message, false, sender.getNumber().get(), getSelfAddress(), ignoreAttachments);
+
+ if (content.isNeedsReceipt()) {
+ actions.add(new SendReceiptAction(sender, message.getTimestamp()));
+ }
+
+ actions.addAll(handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments));
}
if (content.getSyncMessage().isPresent()) {
account.setMultiDevice(true);
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
if (syncMessage.getSent().isPresent()) {
SentTranscriptMessage message = syncMessage.getSent().get();
- handleSignalServiceDataMessage(message.getMessage(), true, sender.getNumber().get(), message.getDestination().orNull(), ignoreAttachments);
+ actions.addAll(handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments));
}
if (syncMessage.getRequest().isPresent()) {
RequestMessage rm = syncMessage.getRequest().get();
if (rm.isContactsRequest()) {
- try {
- sendContacts();
- } catch (UntrustedIdentityException | IOException e) {
- e.printStackTrace();
- }
+ actions.add(SendSyncContactsAction.create());
}
if (rm.isGroupsRequest()) {
- try {
- sendGroups();
- } catch (UntrustedIdentityException | IOException e) {
- e.printStackTrace();
- }
+ actions.add(SendSyncGroupsAction.create());
}
if (rm.isBlockedListRequest()) {
- try {
- sendBlockedList();
- } catch (UntrustedIdentityException | IOException e) {
- e.printStackTrace();
- }
+ actions.add(SendSyncBlockedListAction.create());
}
// TODO Handle rm.isConfigurationRequest();
}
@@ -1309,8 +1604,16 @@ public class Manager implements Signal {
if (g.getName().isPresent()) {
syncGroup.name = g.getName().get();
}
- syncGroup.addMembers(g.getMembers());
- syncGroup.active = g.isActive();
+ syncGroup.addMembers(g.getMembers()
+ .stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toSet()));
+ if (!g.isActive()) {
+ syncGroup.removeMember(account.getSelfAddress());
+ } else {
+ // Add ourself to the member set as it's marked as active
+ syncGroup.addMembers(Collections.singleton(account.getSelfAddress()));
+ }
syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) {
syncGroup.color = g.getColor().get();
@@ -1319,6 +1622,8 @@ public class Manager implements Signal {
if (g.getAvatar().isPresent()) {
retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
}
+ syncGroup.inboxPosition = g.getInboxPosition().orNull();
+ syncGroup.archived = g.isArchived();
account.getGroupStore().updateGroup(syncGroup);
}
}
@@ -1337,13 +1642,7 @@ public class Manager implements Signal {
if (syncMessage.getBlockedList().isPresent()) {
final BlockedListMessage blockedListMessage = syncMessage.getBlockedList().get();
for (SignalServiceAddress address : blockedListMessage.getAddresses()) {
- if (address.getNumber().isPresent()) {
- try {
- setContactBlocked(address.getNumber().get(), true);
- } catch (InvalidNumberException e) {
- e.printStackTrace();
- }
- }
+ setContactBlocked(resolveSignalServiceAddress(address), true);
}
for (byte[] groupId : blockedListMessage.getGroupIds()) {
try {
@@ -1368,10 +1667,10 @@ public class Manager implements Signal {
if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
account.setProfileKey(c.getProfileKey().get());
}
- ContactInfo contact = account.getContactStore().getContact(c.getAddress().getNumber().get());
+ final SignalServiceAddress address = resolveSignalServiceAddress(c.getAddress());
+ ContactInfo contact = account.getContactStore().getContact(address);
if (contact == null) {
- contact = new ContactInfo();
- contact.number = c.getAddress().getNumber().get();
+ contact = new ContactInfo(address);
}
if (c.getName().isPresent()) {
contact.name = c.getName().get();
@@ -1380,22 +1679,18 @@ public class Manager implements Signal {
contact.color = c.getColor().get();
}
if (c.getProfileKey().isPresent()) {
- contact.profileKey = Base64.encodeBytes(c.getProfileKey().get());
+ contact.profileKey = Base64.encodeBytes(c.getProfileKey().get().serialize());
}
if (c.getVerified().isPresent()) {
final VerifiedMessage verifiedMessage = c.getVerified().get();
- account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+ account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (c.getExpirationTimer().isPresent()) {
- ThreadInfo thread = account.getThreadStore().getThread(c.getAddress().getNumber().get());
- if (thread == null) {
- thread = new ThreadInfo();
- thread.id = c.getAddress().getNumber().get();
- }
- thread.messageExpirationTime = c.getExpirationTimer().get();
- account.getThreadStore().updateThread(thread);
+ contact.messageExpirationTime = c.getExpirationTimer().get();
}
contact.blocked = c.isBlocked();
+ contact.inboxPosition = c.getInboxPosition().orNull();
+ contact.archived = c.isArchived();
account.getContactStore().updateContact(contact);
if (c.getAvatar().isPresent()) {
@@ -1417,21 +1712,22 @@ public class Manager implements Signal {
}
if (syncMessage.getVerified().isPresent()) {
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
- account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+ account.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (syncMessage.getConfiguration().isPresent()) {
// TODO
}
}
}
+ return actions;
}
private File getContactAvatarFile(String number) {
- return new File(avatarsPath, "contact-" + number);
+ return new File(pathConfig.getAvatarsPath(), "contact-" + number);
}
- private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
- IOUtils.createPrivateDirectories(avatarsPath);
+ private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException, MissingConfigurationException {
+ IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
return retrieveAttachment(pointer, getContactAvatarFile(number), false);
@@ -1442,11 +1738,11 @@ public class Manager implements Signal {
}
private File getGroupAvatarFile(byte[] groupId) {
- return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
+ return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
}
- private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
- IOUtils.createPrivateDirectories(avatarsPath);
+ private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException, MissingConfigurationException {
+ IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
@@ -1456,16 +1752,39 @@ public class Manager implements Signal {
}
}
- public File getAttachmentFile(long attachmentId) {
- return new File(attachmentsPath, attachmentId + "");
+ private File getProfileAvatarFile(SignalServiceAddress address) {
+ return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier());
}
- private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
- IOUtils.createPrivateDirectories(attachmentsPath);
- return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
+ private File retrieveProfileAvatar(SignalServiceAddress address, String avatarPath, ProfileKey profileKey) throws IOException {
+ IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
+ SignalServiceMessageReceiver receiver = getMessageReceiver();
+ File outputFile = getProfileAvatarFile(address);
+
+ File tmpFile = IOUtils.createTempFile();
+ try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
+ // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
+ IOUtils.copyStreamToFile(input, outputFile, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
+ } finally {
+ try {
+ Files.delete(tmpFile.toPath());
+ } catch (IOException e) {
+ System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage());
+ }
+ }
+ return outputFile;
}
- private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
+ public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
+ return new File(pathConfig.getAttachmentsPath(), attachmentId.toString());
+ }
+
+ private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException, MissingConfigurationException {
+ IOUtils.createPrivateDirectories(pathConfig.getAttachmentsPath());
+ return retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()), true);
+ }
+
+ private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException, MissingConfigurationException {
if (storePreview && pointer.getPreview().isPresent()) {
File previewFile = new File(outputFile + ".preview");
try (OutputStream output = new FileOutputStream(previewFile)) {
@@ -1480,18 +1799,8 @@ public class Manager implements Signal {
final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
File tmpFile = IOUtils.createTempFile();
- try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE)) {
- try (OutputStream output = new FileOutputStream(outputFile)) {
- byte[] buffer = new byte[4096];
- int read;
-
- while ((read = input.read(buffer)) != -1) {
- output.write(buffer, 0, read);
- }
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- return null;
- }
+ try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE)) {
+ IOUtils.copyStreamToFile(input, outputFile);
} finally {
try {
Files.delete(tmpFile.toPath());
@@ -1502,28 +1811,22 @@ public class Manager implements Signal {
return outputFile;
}
- private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException {
+ private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException, MissingConfigurationException {
final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
- return messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE);
+ return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
}
- @Override
- public boolean isRemote() {
- return false;
- }
-
- private void sendGroups() throws IOException, UntrustedIdentityException {
+ void sendGroups() throws IOException, UntrustedIdentityException {
File groupsFile = IOUtils.createTempFile();
try {
try (OutputStream fos = new FileOutputStream(groupsFile)) {
DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
for (GroupInfo record : account.getGroupStore().getGroups()) {
- ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId));
out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId),
- record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null),
- Optional.fromNullable(record.color), record.blocked));
+ record.isMember(account.getSelfAddress()), Optional.of(record.messageExpirationTime),
+ Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived));
}
}
@@ -1555,33 +1858,30 @@ public class Manager implements Signal {
DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
for (ContactInfo record : account.getContactStore().getContacts()) {
VerifiedMessage verifiedMessage = null;
- ThreadInfo info = account.getThreadStore().getThread(record.number);
- if (getIdentities().containsKey(record.number)) {
- JsonIdentityKeyStore.Identity currentIdentity = null;
- for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) {
- if (currentIdentity == null || id.getDateAdded().after(currentIdentity.getDateAdded())) {
- currentIdentity = id;
- }
- }
- if (currentIdentity != null) {
- verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime());
- }
+ JsonIdentityKeyStore.Identity currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress());
+ if (currentIdentity != null) {
+ verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime());
}
- byte[] profileKey = record.profileKey == null ? null : Base64.decode(record.profileKey);
+ ProfileKey profileKey = null;
+ try {
+ profileKey = record.profileKey == null ? null : new ProfileKey(Base64.decode(record.profileKey));
+ } catch (InvalidInputException ignored) {
+ }
out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name),
createContactAvatarAttachment(record.number), Optional.fromNullable(record.color),
Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked,
- Optional.fromNullable(info != null ? info.messageExpirationTime : null)));
+ Optional.of(record.messageExpirationTime),
+ Optional.fromNullable(record.inboxPosition), record.archived));
}
if (account.getProfileKey() != null) {
// Send our own profile key as well
out.write(new DeviceContact(account.getSelfAddress(),
- Optional.absent(), Optional.absent(),
- Optional.absent(), Optional.absent(),
+ Optional.absent(), Optional.absent(),
+ Optional.absent(), Optional.absent(),
Optional.of(account.getProfileKey()),
- false, Optional.absent()));
+ false, Optional.absent(), Optional.absent(), false));
}
}
@@ -1605,7 +1905,7 @@ public class Manager implements Signal {
}
}
- private void sendBlockedList() throws IOException, UntrustedIdentityException {
+ void sendBlockedList() throws IOException, UntrustedIdentityException {
List addresses = new ArrayList<>();
for (ContactInfo record : account.getContactStore().getContacts()) {
if (record.blocked) {
@@ -1631,20 +1931,19 @@ public class Manager implements Signal {
}
public ContactInfo getContact(String number) {
- return account.getContactStore().getContact(number);
+ return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number));
}
public GroupInfo getGroup(byte[] groupId) {
return account.getGroupStore().getGroup(groupId);
}
- public Map> getIdentities() {
+ public List getIdentities() {
return account.getSignalProtocolStore().getIdentities();
}
- public Pair> getIdentities(String number) throws InvalidNumberException {
- String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
- return new Pair<>(canonicalizedNumber, account.getSignalProtocolStore().getIdentities(canonicalizedNumber));
+ public List getIdentities(String number) throws InvalidNumberException {
+ return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number));
}
/**
@@ -1653,8 +1952,9 @@ public class Manager implements Signal {
* @param name username of the identity
* @param fingerprint Fingerprint
*/
- public boolean trustIdentityVerified(String name, byte[] fingerprint) {
- List ids = account.getSignalProtocolStore().getIdentities(name);
+ public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException {
+ SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name);
+ List ids = account.getSignalProtocolStore().getIdentities(address);
if (ids == null) {
return false;
}
@@ -1663,9 +1963,9 @@ public class Manager implements Signal {
continue;
}
- account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
try {
- sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
e.printStackTrace();
}
@@ -1681,19 +1981,20 @@ public class Manager implements Signal {
* @param name username of the identity
* @param safetyNumber Safety number
*/
- public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) {
- List ids = account.getSignalProtocolStore().getIdentities(name);
+ public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException {
+ SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name);
+ List ids = account.getSignalProtocolStore().getIdentities(address);
if (ids == null) {
return false;
}
for (JsonIdentityKeyStore.Identity id : ids) {
- if (!safetyNumber.equals(computeSafetyNumber(name, id.getIdentityKey()))) {
+ if (!safetyNumber.equals(computeSafetyNumber(address, id.getIdentityKey()))) {
continue;
}
- account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
try {
- sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
e.printStackTrace();
}
@@ -1709,15 +2010,16 @@ public class Manager implements Signal {
* @param name username of the identity
*/
public boolean trustIdentityAllKeys(String name) {
- List ids = account.getSignalProtocolStore().getIdentities(name);
+ SignalServiceAddress address = resolveSignalServiceAddress(name);
+ List ids = account.getSignalProtocolStore().getIdentities(address);
if (ids == null) {
return false;
}
for (JsonIdentityKeyStore.Identity id : ids) {
if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
- account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
+ account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
try {
- sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
+ sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
} catch (IOException | UntrustedIdentityException e) {
e.printStackTrace();
}
@@ -1727,8 +2029,46 @@ public class Manager implements Signal {
return true;
}
- public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
- return Utils.computeSafetyNumber(username, getIdentity(), theirUsername, theirIdentityKey);
+ public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
+ return Utils.computeSafetyNumber(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey);
+ }
+
+ void saveAccount() {
+ account.save();
+ }
+
+ public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
+ String canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : Util.canonicalizeNumber(identifier, account.getUsername());
+ return resolveSignalServiceAddress(canonicalizedNumber);
+ }
+
+ public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
+ SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier);
+
+ return resolveSignalServiceAddress(address);
+ }
+
+ public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
+ if (address.matches(account.getSelfAddress())) {
+ return account.getSelfAddress();
+ }
+
+ return account.getRecipientStore().resolveServiceAddress(address);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (messagePipe != null) {
+ messagePipe.shutdown();
+ messagePipe = null;
+ }
+
+ if (unidentifiedMessagePipe != null) {
+ unidentifiedMessagePipe.shutdown();
+ unidentifiedMessagePipe = null;
+ }
+
+ account.close();
}
public interface ReceiveMessageHandler {
diff --git a/src/main/java/org/asamk/signal/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java
similarity index 58%
rename from src/main/java/org/asamk/signal/NotAGroupMemberException.java
rename to src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java
index cfdc7855..8c0e9be0 100644
--- a/src/main/java/org/asamk/signal/NotAGroupMemberException.java
+++ b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java
@@ -1,9 +1,8 @@
-package org.asamk.signal;
+package org.asamk.signal.manager;
-import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64;
-public class NotAGroupMemberException extends DBusExecutionException {
+public class NotAGroupMemberException extends Exception {
public NotAGroupMemberException(byte[] groupId, String groupName) {
super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")");
diff --git a/src/main/java/org/asamk/signal/manager/PathConfig.java b/src/main/java/org/asamk/signal/manager/PathConfig.java
new file mode 100644
index 00000000..2c2d938a
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/PathConfig.java
@@ -0,0 +1,34 @@
+package org.asamk.signal.manager;
+
+public class PathConfig {
+
+ private final String dataPath;
+ private final String attachmentsPath;
+ private final String avatarsPath;
+
+ public static PathConfig createDefault(final String settingsPath) {
+ return new PathConfig(
+ settingsPath + "/data",
+ settingsPath + "/attachments",
+ settingsPath + "/avatars"
+ );
+ }
+
+ private PathConfig(final String dataPath, final String attachmentsPath, final String avatarsPath) {
+ this.dataPath = dataPath;
+ this.attachmentsPath = attachmentsPath;
+ this.avatarsPath = avatarsPath;
+ }
+
+ public String getDataPath() {
+ return dataPath;
+ }
+
+ public String getAttachmentsPath() {
+ return attachmentsPath;
+ }
+
+ public String getAvatarsPath() {
+ return avatarsPath;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
new file mode 100644
index 00000000..4f1aca18
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
@@ -0,0 +1,117 @@
+/*
+ Copyright (C) 2015-2020 AsamK and contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+package org.asamk.signal.manager;
+
+import org.asamk.signal.storage.SignalAccount;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.profiles.ProfileKey;
+import org.whispersystems.libsignal.IdentityKeyPair;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.libsignal.util.KeyHelper;
+import org.whispersystems.signalservice.api.SignalServiceAccountManager;
+import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.util.SleepTimer;
+import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
+import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
+import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
+
+import java.io.IOException;
+import java.util.concurrent.TimeoutException;
+
+public class ProvisioningManager {
+
+ private final PathConfig pathConfig;
+ private final SignalServiceConfiguration serviceConfiguration;
+ private final String userAgent;
+
+ private final SignalServiceAccountManager accountManager;
+ private final IdentityKeyPair identityKey;
+ private final int registrationId;
+ private final String password;
+
+ public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) {
+ this.pathConfig = PathConfig.createDefault(settingsPath);
+ this.serviceConfiguration = serviceConfiguration;
+ this.userAgent = userAgent;
+
+ identityKey = KeyHelper.generateIdentityKeyPair();
+ registrationId = KeyHelper.generateRegistrationId(false);
+ password = KeyUtils.createPassword();
+ final SleepTimer timer = new UptimeSleepTimer();
+ GroupsV2Operations groupsV2Operations;
+ try {
+ groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration));
+ } catch (Throwable ignored) {
+ groupsV2Operations = null;
+ }
+ accountManager = new SignalServiceAccountManager(serviceConfiguration,
+ new DynamicCredentialsProvider(null, null, password, null, SignalServiceAddress.DEFAULT_DEVICE_ID),
+ userAgent,
+ groupsV2Operations,
+ timer);
+ }
+
+ public String getDeviceLinkUri() throws TimeoutException, IOException {
+ String deviceUuid = accountManager.getNewDeviceUuid();
+
+ return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey()));
+ }
+
+ public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
+ String signalingKey = KeyUtils.createSignalingKey();
+ SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(identityKey, signalingKey, false, true, registrationId, deviceName);
+
+ String username = ret.getNumber();
+ // TODO do this check before actually registering
+ if (SignalAccount.userExists(pathConfig.getDataPath(), username)) {
+ throw new UserAlreadyExists(username, SignalAccount.getFileName(pathConfig.getDataPath(), username));
+ }
+
+ // Create new account with the synced identity
+ byte[] profileKeyBytes = ret.getProfileKey();
+ ProfileKey profileKey;
+ if (profileKeyBytes == null) {
+ profileKey = KeyUtils.createProfileKey();
+ } else {
+ try {
+ profileKey = new ProfileKey(profileKeyBytes);
+ } catch (InvalidInputException e) {
+ throw new IOException("Received invalid profileKey", e);
+ }
+ }
+
+ try (SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(), username, ret.getUuid(), password, ret.getDeviceId(), ret.getIdentity(), registrationId, signalingKey, profileKey)) {
+ account.save();
+
+ try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) {
+
+ m.refreshPreKeys();
+
+ m.requestSyncGroups();
+ m.requestSyncContacts();
+ m.requestSyncBlocked();
+ m.requestSyncConfiguration();
+
+ m.saveAccount();
+ }
+ }
+
+ return username;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java
new file mode 100644
index 00000000..a8b0c5b6
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java
@@ -0,0 +1,80 @@
+package org.asamk.signal.manager;
+
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
+import org.whispersystems.signalservice.api.push.TrustStore;
+import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
+import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
+import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
+import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
+import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
+import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
+import org.whispersystems.util.Base64;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import okhttp3.Dns;
+import okhttp3.Interceptor;
+
+public class ServiceConfig {
+
+ final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
+ final static int PREKEY_MINIMUM_COUNT = 20;
+ final static int PREKEY_BATCH_SIZE = 100;
+ final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
+ final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
+
+ private final static String URL = "https://textsecure-service.whispersystems.org";
+ private final static String CDN_URL = "https://cdn.signal.org";
+ private final static String CDN2_URL = "https://cdn2.signal.org";
+ private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org";
+ private final static String STORAGE_URL = "https://storage.signal.org";
+ private final static TrustStore TRUST_STORE = new WhisperTrustStore();
+
+ private final static Optional dns = Optional.absent();
+
+ private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=";
+
+ static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false, false);
+
+ public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) {
+ final Interceptor userAgentInterceptor = chain ->
+ chain.proceed(chain.request().newBuilder()
+ .header("User-Agent", userAgent)
+ .build());
+
+ final List interceptors = Collections.singletonList(userAgentInterceptor);
+
+ final byte[] zkGroupServerPublicParams;
+ try {
+ zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+
+ return new SignalServiceConfiguration(
+ new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
+ makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
+ new SignalContactDiscoveryUrl[0],
+ new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
+ new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
+ interceptors,
+ dns,
+ zkGroupServerPublicParams
+ );
+ }
+
+ private static Map makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) {
+ Map result = new HashMap<>();
+ result.put(0, cdn0Urls);
+ result.put(2, cdn2Urls);
+ return Collections.unmodifiableMap(result);
+ }
+
+ private ServiceConfig() {
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java b/src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java
new file mode 100644
index 00000000..52869acd
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager;
+
+public class StickerPackInvalidException extends Exception {
+
+ public StickerPackInvalidException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/asamk/signal/TrustLevel.java b/src/main/java/org/asamk/signal/manager/TrustLevel.java
similarity index 97%
rename from src/main/java/org/asamk/signal/TrustLevel.java
rename to src/main/java/org/asamk/signal/manager/TrustLevel.java
index 5eaf960a..c9fa7a5e 100644
--- a/src/main/java/org/asamk/signal/TrustLevel.java
+++ b/src/main/java/org/asamk/signal/manager/TrustLevel.java
@@ -1,4 +1,4 @@
-package org.asamk.signal;
+package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
diff --git a/src/main/java/org/asamk/signal/UserAlreadyExists.java b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java
similarity index 92%
rename from src/main/java/org/asamk/signal/UserAlreadyExists.java
rename to src/main/java/org/asamk/signal/manager/UserAlreadyExists.java
index 28836f28..a07c455b 100644
--- a/src/main/java/org/asamk/signal/UserAlreadyExists.java
+++ b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java
@@ -1,4 +1,4 @@
-package org.asamk.signal;
+package org.asamk.signal.manager;
public class UserAlreadyExists extends Exception {
diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java
index b253a2ee..05fcfb5e 100644
--- a/src/main/java/org/asamk/signal/manager/Utils.java
+++ b/src/main/java/org/asamk/signal/manager/Utils.java
@@ -1,6 +1,5 @@
package org.asamk.signal.manager;
-import org.asamk.signal.AttachmentInvalidException;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -13,9 +12,9 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.api.util.InvalidNumberException;
-import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.StreamDetails;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import org.whispersystems.util.Base64;
import java.io.BufferedInputStream;
@@ -35,12 +34,10 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
+import java.util.UUID;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
@@ -61,7 +58,7 @@ class Utils {
return signalServiceAttachments;
}
- private static String getFileMimeType(File file) throws IOException {
+ static String getFileMimeType(File file, String defaultMimeType) throws IOException {
String mime = Files.probeContentType(file.toPath());
if (mime == null) {
try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
@@ -69,7 +66,7 @@ class Utils {
}
}
if (mime == null) {
- mime = "application/octet-stream";
+ return defaultMimeType;
}
return mime;
}
@@ -77,12 +74,14 @@ class Utils {
static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length();
- final String mime = getFileMimeType(attachmentFile);
- // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option
+ final String mime = getFileMimeType(attachmentFile, "application/octet-stream");
+ // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option
+ final long uploadTimestamp = System.currentTimeMillis();
Optional preview = Optional.absent();
Optional caption = Optional.absent();
Optional blurHash = Optional.absent();
- return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, blurHash, null);
+ final Optional resumableUploadSpec = Optional.absent();
+ return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, false, preview, 0, 0, uploadTimestamp, caption, blurHash, null, null, resumableUploadSpec);
}
static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
@@ -97,7 +96,7 @@ class Utils {
static CertificateValidator getCertificateValidator() {
try {
- ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
+ ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
return new CertificateValidator(unidentifiedSenderTrustRoot);
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
@@ -149,38 +148,19 @@ class Utils {
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
}
- static Set getSignalServiceAddresses(Collection recipients, String localNumber) {
- Set recipientsTS = new HashSet<>(recipients.size());
- for (String recipient : recipients) {
- try {
- recipientsTS.add(getPushAddress(recipient, localNumber));
- } catch (InvalidNumberException e) {
- System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
- System.err.println("Aborting sending.");
- return null;
- }
- }
- return recipientsTS;
- }
-
- static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {
- return PhoneNumberFormatter.formatNumber(number, localNumber);
- }
-
- private static SignalServiceAddress getPushAddress(String number, String localNumber) throws InvalidNumberException {
- String e164number = canonicalizeNumber(number, localNumber);
- return new SignalServiceAddress(null, e164number);
- }
-
static SignalServiceEnvelope loadEnvelope(File file) throws IOException {
try (FileInputStream f = new FileInputStream(file)) {
DataInputStream in = new DataInputStream(f);
int version = in.readInt();
- if (version > 2) {
+ if (version > 4) {
return null;
}
int type = in.readInt();
String source = in.readUTF();
+ UUID sourceUuid = null;
+ if (version >= 3) {
+ sourceUuid = UuidUtil.parseOrNull(in.readUTF());
+ }
int sourceDevice = in.readInt();
if (version == 1) {
// read legacy relay field
@@ -199,25 +179,33 @@ class Utils {
legacyMessage = new byte[legacyMessageLen];
in.readFully(legacyMessage);
}
- long serverTimestamp = 0;
+ long serverReceivedTimestamp = 0;
String uuid = null;
- if (version == 2) {
- serverTimestamp = in.readLong();
+ if (version >= 2) {
+ serverReceivedTimestamp = in.readLong();
uuid = in.readUTF();
if ("".equals(uuid)) {
uuid = null;
}
}
- return new SignalServiceEnvelope(type, Optional.of(new SignalServiceAddress(null, source)), sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid);
+ long serverDeliveredTimestamp = 0;
+ if (version >= 4) {
+ serverDeliveredTimestamp = in.readLong();
+ }
+ Optional addressOptional = sourceUuid == null && source.isEmpty()
+ ? Optional.absent()
+ : Optional.of(new SignalServiceAddress(sourceUuid, source));
+ return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverReceivedTimestamp, serverDeliveredTimestamp, uuid);
}
}
static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
try (FileOutputStream f = new FileOutputStream(file)) {
try (DataOutputStream out = new DataOutputStream(f)) {
- out.writeInt(2); // version
+ out.writeInt(4); // version
out.writeInt(envelope.getType());
- out.writeUTF(envelope.getSourceE164().get());
+ out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : "");
+ out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : "");
out.writeInt(envelope.getSourceDevice());
out.writeLong(envelope.getTimestamp());
if (envelope.hasContent()) {
@@ -232,9 +220,10 @@ class Utils {
} else {
out.writeInt(0);
}
- out.writeLong(envelope.getServerTimestamp());
+ out.writeLong(envelope.getServerReceivedTimestamp());
String uuid = envelope.getUuid();
out.writeUTF(uuid == null ? "" : uuid);
+ out.writeLong(envelope.getServerDeliveredTimestamp());
}
}
}
@@ -256,10 +245,28 @@ class Utils {
return outputFile;
}
- static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) {
- // Version 1: E164 user
- // Version 2: UUID user
- Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(1, ownUsername.getBytes(), ownIdentityKey, theirUsername.getBytes(), theirIdentityKey);
+ static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
+ int version;
+ byte[] ownId;
+ byte[] theirId;
+
+ if (ServiceConfig.capabilities.isUuid()
+ && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) {
+ // Version 2: UUID user
+ version = 2;
+ ownId = UuidUtil.toByteArray(ownAddress.getUuid().get());
+ theirId = UuidUtil.toByteArray(theirAddress.getUuid().get());
+ } else {
+ // Version 1: E164 user
+ version = 1;
+ if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) {
+ return "INVALID ID";
+ }
+ ownId = ownAddress.getNumber().get().getBytes();
+ theirId = theirAddress.getNumber().get().getBytes();
+ }
+
+ Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey);
return fingerprint.getDisplayableFingerprint().getDisplayText();
}
diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java
index 1ba78f6b..6043d803 100644
--- a/src/main/java/org/asamk/signal/storage/SignalAccount.java
+++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java
@@ -10,39 +10,57 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.contacts.JsonContactsStore;
+import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.JsonGroupStore;
+import org.asamk.signal.storage.profiles.ProfileStore;
+import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
-import org.asamk.signal.storage.threads.JsonThreadStore;
+import org.asamk.signal.storage.protocol.RecipientStore;
+import org.asamk.signal.storage.protocol.SessionInfo;
+import org.asamk.signal.storage.protocol.SignalServiceAddressResolver;
+import org.asamk.signal.storage.threads.LegacyJsonThreadStore;
+import org.asamk.signal.storage.threads.ThreadInfo;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium;
+import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
+import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Collection;
+import java.util.UUID;
+import java.util.stream.Collectors;
-public class SignalAccount {
+public class SignalAccount implements Closeable {
private final ObjectMapper jsonProcessor = new ObjectMapper();
- private FileChannel fileChannel;
- private FileLock lock;
+ private final FileChannel fileChannel;
+ private final FileLock lock;
private String username;
+ private UUID uuid;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
private boolean isMultiDevice = false;
private String password;
private String registrationLockPin;
private String signalingKey;
- private byte[] profileKey;
+ private ProfileKey profileKey;
private int preKeyIdOffset;
private int nextSignedPreKeyId;
@@ -51,72 +69,82 @@ public class SignalAccount {
private JsonSignalProtocolStore signalProtocolStore;
private JsonGroupStore groupStore;
private JsonContactsStore contactStore;
- private JsonThreadStore threadStore;
+ private RecipientStore recipientStore;
+ private ProfileStore profileStore;
- private SignalAccount() {
+ private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
+ this.fileChannel = fileChannel;
+ this.lock = lock;
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
- jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
public static SignalAccount load(String dataPath, String username) throws IOException {
- SignalAccount account = new SignalAccount();
- IOUtils.createPrivateDirectories(dataPath);
- account.openFileChannel(getFileName(dataPath, username));
- account.load();
- return account;
+ final String fileName = getFileName(dataPath, username);
+ final Pair pair = openFileChannel(fileName);
+ try {
+ SignalAccount account = new SignalAccount(pair.first(), pair.second());
+ account.load();
+ return account;
+ } catch (Throwable e) {
+ pair.second().close();
+ pair.first().close();
+ throw e;
+ }
}
- public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, byte[] profileKey) throws IOException {
+ public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
+ String fileName = getFileName(dataPath, username);
+ if (!new File(fileName).exists()) {
+ IOUtils.createPrivateFile(fileName);
+ }
- SignalAccount account = new SignalAccount();
- account.openFileChannel(getFileName(dataPath, username));
+ final Pair pair = openFileChannel(fileName);
+ SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.username = username;
account.profileKey = profileKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore();
- account.threadStore = new JsonThreadStore();
account.contactStore = new JsonContactsStore();
+ account.recipientStore = new RecipientStore();
+ account.profileStore = new ProfileStore();
account.registered = false;
return account;
}
- public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, byte[] profileKey) throws IOException {
+ public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
+ String fileName = getFileName(dataPath, username);
+ if (!new File(fileName).exists()) {
+ IOUtils.createPrivateFile(fileName);
+ }
- SignalAccount account = new SignalAccount();
- account.openFileChannel(getFileName(dataPath, username));
+ final Pair pair = openFileChannel(fileName);
+ SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.username = username;
+ account.uuid = uuid;
account.password = password;
account.profileKey = profileKey;
account.deviceId = deviceId;
account.signalingKey = signalingKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore();
- account.threadStore = new JsonThreadStore();
account.contactStore = new JsonContactsStore();
+ account.recipientStore = new RecipientStore();
+ account.profileStore = new ProfileStore();
account.registered = true;
account.isMultiDevice = true;
return account;
}
- public static SignalAccount createTemporaryAccount(IdentityKeyPair identityKey, int registrationId) {
- SignalAccount account = new SignalAccount();
-
- account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
- account.registered = false;
-
- return account;
- }
-
public static String getFileName(String dataPath, String username) {
return dataPath + "/" + username;
}
@@ -136,6 +164,14 @@ public class SignalAccount {
rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
}
+ JsonNode uuidNode = rootNode.get("uuid");
+ if (uuidNode != null && !uuidNode.isNull()) {
+ try {
+ uuid = UUID.fromString(uuidNode.asText());
+ } catch (IllegalArgumentException e) {
+ throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
+ }
+ }
JsonNode node = rootNode.get("deviceId");
if (node != null) {
deviceId = node.asInt();
@@ -161,7 +197,11 @@ public class SignalAccount {
nextSignedPreKeyId = 0;
}
if (rootNode.has("profileKey")) {
- profileKey = Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText());
+ try {
+ profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
+ } catch (InvalidInputException e) {
+ throw new IOException("Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", e);
+ }
}
signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
@@ -181,12 +221,66 @@ public class SignalAccount {
if (contactStore == null) {
contactStore = new JsonContactsStore();
}
+
+ JsonNode recipientStoreNode = rootNode.get("recipientStore");
+ if (recipientStoreNode != null) {
+ recipientStore = jsonProcessor.convertValue(recipientStoreNode, RecipientStore.class);
+ }
+ if (recipientStore == null) {
+ recipientStore = new RecipientStore();
+
+ recipientStore.resolveServiceAddress(getSelfAddress());
+
+ for (ContactInfo contact : contactStore.getContacts()) {
+ recipientStore.resolveServiceAddress(contact.getAddress());
+ }
+
+ for (GroupInfo group : groupStore.getGroups()) {
+ group.members = group.members.stream()
+ .map(m -> recipientStore.resolveServiceAddress(m))
+ .collect(Collectors.toSet());
+ }
+
+ for (SessionInfo session : signalProtocolStore.getSessions()) {
+ session.address = recipientStore.resolveServiceAddress(session.address);
+ }
+
+ for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) {
+ identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
+ }
+ }
+
+ JsonNode profileStoreNode = rootNode.get("profileStore");
+ if (profileStoreNode != null) {
+ profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
+ }
+ if (profileStore == null) {
+ profileStore = new ProfileStore();
+ }
+
JsonNode threadStoreNode = rootNode.get("threadStore");
if (threadStoreNode != null) {
- threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
- }
- if (threadStore == null) {
- threadStore = new JsonThreadStore();
+ LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
+ // Migrate thread info to group and contact store
+ for (ThreadInfo thread : threadStore.getThreads()) {
+ if (thread.id == null || thread.id.isEmpty()) {
+ continue;
+ }
+ try {
+ ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
+ if (contactInfo != null) {
+ contactInfo.messageExpirationTime = thread.messageExpirationTime;
+ contactStore.updateContact(contactInfo);
+ } else {
+ GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
+ if (groupInfo != null) {
+ groupInfo.messageExpirationTime = thread.messageExpirationTime;
+ groupStore.updateGroup(groupInfo);
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ }
}
}
@@ -196,6 +290,7 @@ public class SignalAccount {
}
ObjectNode rootNode = jsonProcessor.createObjectNode();
rootNode.put("username", username)
+ .put("uuid", uuid == null ? null : uuid.toString())
.put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice)
.put("password", password)
@@ -203,40 +298,44 @@ public class SignalAccount {
.put("signalingKey", signalingKey)
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
- .put("profileKey", Base64.encodeBytes(profileKey))
+ .put("profileKey", Base64.encodeBytes(profileKey.serialize()))
.put("registered", registered)
.putPOJO("axolotlStore", signalProtocolStore)
.putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore)
- .putPOJO("threadStore", threadStore)
+ .putPOJO("recipientStore", recipientStore)
+ .putPOJO("profileStore", profileStore)
;
try {
- synchronized (fileChannel) {
- fileChannel.position(0);
- jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
- fileChannel.truncate(fileChannel.position());
- fileChannel.force(false);
+ try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+ // Write to memory first to prevent corrupting the file in case of serialization errors
+ jsonProcessor.writeValue(output, rootNode);
+ ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
+ synchronized (fileChannel) {
+ fileChannel.position(0);
+ input.transferTo(Channels.newOutputStream(fileChannel));
+ fileChannel.truncate(fileChannel.position());
+ fileChannel.force(false);
+ }
}
} catch (Exception e) {
System.err.println(String.format("Error saving file: %s", e.getMessage()));
}
}
- private void openFileChannel(String fileName) throws IOException {
- if (fileChannel != null) {
- return;
- }
-
- if (!new File(fileName).exists()) {
- IOUtils.createPrivateFile(fileName);
- }
- fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
- lock = fileChannel.tryLock();
+ private static Pair openFileChannel(String fileName) throws IOException {
+ FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
+ FileLock lock = fileChannel.tryLock();
if (lock == null) {
System.err.println("Config file is in use by another instance, waiting…");
lock = fileChannel.lock();
System.err.println("Config file lock acquired.");
}
+ return new Pair<>(fileChannel, lock);
+ }
+
+ public void setResolver(final SignalServiceAddressResolver resolver) {
+ signalProtocolStore.setResolver(resolver);
}
public void addPreKeys(Collection records) {
@@ -263,16 +362,28 @@ public class SignalAccount {
return contactStore;
}
- public JsonThreadStore getThreadStore() {
- return threadStore;
+ public RecipientStore getRecipientStore() {
+ return recipientStore;
+ }
+
+ public ProfileStore getProfileStore() {
+ return profileStore;
}
public String getUsername() {
return username;
}
+ public UUID getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(final UUID uuid) {
+ this.uuid = uuid;
+ }
+
public SignalServiceAddress getSelfAddress() {
- return new SignalServiceAddress(null, username);
+ return new SignalServiceAddress(uuid, username);
}
public int getDeviceId() {
@@ -291,6 +402,10 @@ public class SignalAccount {
return registrationLockPin;
}
+ public String getRegistrationLock() {
+ return null; // TODO implement KBS
+ }
+
public void setRegistrationLockPin(final String registrationLockPin) {
this.registrationLockPin = registrationLockPin;
}
@@ -303,11 +418,11 @@ public class SignalAccount {
this.signalingKey = signalingKey;
}
- public byte[] getProfileKey() {
+ public ProfileKey getProfileKey() {
return profileKey;
}
- public void setProfileKey(final byte[] profileKey) {
+ public void setProfileKey(final ProfileKey profileKey) {
this.profileKey = profileKey;
}
@@ -334,4 +449,15 @@ public class SignalAccount {
public void setMultiDevice(final boolean multiDevice) {
isMultiDevice = multiDevice;
}
+
+ @Override
+ public void close() throws IOException {
+ synchronized (fileChannel) {
+ try {
+ lock.close();
+ } catch (ClosedChannelException ignored) {
+ }
+ fileChannel.close();
+ }
+ }
}
diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
index be69b40c..4d3a5e95 100644
--- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
+++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
@@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import java.util.UUID;
+
public class ContactInfo {
@JsonProperty
@@ -13,17 +15,37 @@ public class ContactInfo {
@JsonProperty
public String number;
+ @JsonProperty
+ public UUID uuid;
+
@JsonProperty
public String color;
+ @JsonProperty(defaultValue = "0")
+ public int messageExpirationTime;
+
@JsonProperty
public String profileKey;
@JsonProperty(defaultValue = "false")
public boolean blocked;
+ @JsonProperty
+ public Integer inboxPosition;
+
+ @JsonProperty(defaultValue = "false")
+ public boolean archived;
+
+ public ContactInfo() {
+ }
+
+ public ContactInfo(SignalServiceAddress address) {
+ this.number = address.getNumber().orNull();
+ this.uuid = address.getUuid().orNull();
+ }
+
@JsonIgnore
public SignalServiceAddress getAddress() {
- return new SignalServiceAddress(null, number);
+ return new SignalServiceAddress(uuid, number);
}
}
diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java
index c10dfbb7..bb81b0c9 100644
--- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java
+++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java
@@ -1,41 +1,46 @@
package org.asamk.signal.storage.contacts;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import java.io.IOException;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
public class JsonContactsStore {
- private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty("contacts")
- @JsonSerialize(using = JsonContactsStore.MapToListSerializer.class)
- @JsonDeserialize(using = ContactsDeserializer.class)
- private Map contacts = new HashMap<>();
+ private List contacts = new ArrayList<>();
public void updateContact(ContactInfo contact) {
- contacts.put(contact.number, contact);
+ final SignalServiceAddress contactAddress = contact.getAddress();
+ for (int i = 0; i < contacts.size(); i++) {
+ if (contacts.get(i).getAddress().matches(contactAddress)) {
+ contacts.set(i, contact);
+ return;
+ }
+ }
+
+ contacts.add(contact);
}
- public ContactInfo getContact(String number) {
- return contacts.get(number);
+ public ContactInfo getContact(SignalServiceAddress address) {
+ for (ContactInfo contact : contacts) {
+ if (contact.getAddress().matches(address)) {
+ if (contact.uuid == null) {
+ contact.uuid = address.getUuid().orNull();
+ } else if (contact.number == null) {
+ contact.number = address.getNumber().orNull();
+ }
+
+ return contact;
+ }
+ }
+ return null;
}
public List getContacts() {
- return new ArrayList<>(contacts.values());
+ return new ArrayList<>(contacts);
}
/**
@@ -44,27 +49,4 @@ public class JsonContactsStore {
public void clear() {
contacts.clear();
}
-
- private static class MapToListSerializer extends JsonSerializer