mirror of
https://github.com/AsamK/signal-cli
synced 2025-09-02 20:40:38 +00:00
Merge remote-tracking branch 'AsamK/master' into master
This commit is contained in:
commit
7aa31842dd
91 changed files with 3917 additions and 1594 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
60
.github/workflows/codeql-analysis.yml
vendored
Normal file
60
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -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
|
22
.idea/codeStyles/Project.xml
generated
22
.idea/codeStyles/Project.xml
generated
|
@ -4,6 +4,28 @@
|
|||
<JavaCodeStyleSettings>
|
||||
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
||||
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
|
||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
<option name="JD_P_AT_EMPTY_LINES" value="false" />
|
||||
</JavaCodeStyleSettings>
|
||||
<XML>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
31
build.gradle
31
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"
|
||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
2
gradlew
vendored
2
gradlew
vendored
|
@ -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
|
||||
|
|
25
gradlew.bat
vendored
25
gradlew.bat
vendored
|
@ -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
|
||||
|
|
|
@ -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": "<STICKER_PACK_TITLE>",
|
||||
"author": "<STICKER_PACK_AUTHOR>",
|
||||
"cover": { // Optional cover, by default the first sticker is used as cover
|
||||
"file": "<name of webp file, mandatory>",
|
||||
"emoji": "<optional>"
|
||||
},
|
||||
"stickers": [
|
||||
{
|
||||
"file": "<name of webp file, mandatory>",
|
||||
"emoji": "<optional>"
|
||||
}
|
||||
...
|
||||
]
|
||||
}
|
||||
----
|
||||
|
||||
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 <asamk@gmx.de>, who is assisted by other open
|
||||
source contributors. For more information about signal-cli development, see
|
||||
Maintained by AsamK <asamk@gmx.de>, who is assisted by other open source contributors.
|
||||
For more information about signal-cli development, see
|
||||
<https://github.com/AsamK/signal-cli>.
|
||||
|
|
|
@ -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<String> attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
|
||||
long sendMessage(String message, List<String> attachments, String recipient) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
|
||||
|
||||
void sendMessage(String message, List<String> attachments, List<String> recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
|
||||
long sendMessage(String message, List<String> attachments, List<String> recipients) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
|
||||
|
||||
void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions;
|
||||
void sendEndSessionMessage(List<String> recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
|
||||
|
||||
void sendGroupMessage(String message, List<String> attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException;
|
||||
long sendGroupMessage(String message, List<String> 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<byte[]> getGroupIds();
|
||||
|
||||
|
@ -35,17 +35,17 @@ public interface Signal extends DBusInterface {
|
|||
|
||||
List<String> getGroupMembers(byte[] groupId);
|
||||
|
||||
byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException;
|
||||
byte[] updateGroup(byte[] groupId, String name, List<String> 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<String> attachments;
|
||||
private final long timestamp;
|
||||
private final String sender;
|
||||
private final byte[] groupId;
|
||||
private final String message;
|
||||
private final List<String> attachments;
|
||||
|
||||
public MessageReceived(String objectpath, long timestamp, String sender, byte[] groupId, String message, List<String> 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<String> attachments;
|
||||
|
||||
public SyncMessageReceived(String objectpath, long timestamp, String source, String destination, byte[] groupId, String message, List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
src/main/java/org/asamk/signal/BaseConfig.java
Normal file
12
src/main/java/org/asamk/signal/BaseConfig.java
Normal file
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<JsonAttachment> 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<>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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<String> getAttachments(SignalServiceDataMessage message, Manager m) {
|
||||
List<String> 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);
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package org.asamk.signal;
|
||||
|
||||
class JsonError {
|
||||
|
||||
String message;
|
||||
|
||||
JsonError(Throwable exception) {
|
||||
this.message = exception.getMessage();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<String> blockedNumbers;
|
||||
List<ReadMessage> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, Command> 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<String, Command> 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<String, Command> 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<String, Command> 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
|
||||
|
|
|
@ -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" : "<unavailable>") + (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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<String, Command> getCommands() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<String, List<JsonIdentityKeyStore.Identity>> 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<String, List<JsonIdentityKeyStore.Identity>> key = m.getIdentities(number);
|
||||
for (JsonIdentityKeyStore.Identity id : key.second()) {
|
||||
printIdentityFingerprint(m, key.first(), id);
|
||||
List<JsonIdentityKeyStore.Identity> 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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<Signal.MessageReceived>() {
|
||||
@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<Signal.ReceiptReceived>() {
|
||||
@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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -21,7 +21,7 @@ public class RemovePinCommand implements LocalCommand {
|
|||
return 1;
|
||||
}
|
||||
try {
|
||||
m.setRegistrationLockPin(Optional.<String>absent());
|
||||
m.setRegistrationLockPin(Optional.absent());
|
||||
return 0;
|
||||
} catch (IOException e) {
|
||||
System.err.println("Remove pin error: " + e.getMessage());
|
||||
|
|
|
@ -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.<String>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<String> attachments = ns.getList("attachment");
|
||||
if (attachments == null) {
|
||||
attachments = new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> 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.<String>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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
205
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
Normal file
205
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
Normal file
|
@ -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<String> attachments, final String recipient) {
|
||||
List<String> 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<String> attachments, final List<String> 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<String> 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<String> 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<byte[]> getGroupIds() {
|
||||
List<GroupInfo> groups = m.getGroups();
|
||||
List<byte[]> 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<String> 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<String> 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
59
src/main/java/org/asamk/signal/json/JsonDataMessage.java
Normal file
59
src/main/java/org/asamk/signal/json/JsonDataMessage.java
Normal file
|
@ -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<JsonAttachment> 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());
|
||||
}
|
||||
}
|
10
src/main/java/org/asamk/signal/json/JsonError.java
Normal file
10
src/main/java/org/asamk/signal/json/JsonError.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
public class JsonError {
|
||||
|
||||
String message;
|
||||
|
||||
public JsonError(Throwable exception) {
|
||||
this.message = exception.getMessage();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal;
|
||||
package org.asamk.signal.json;
|
||||
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
50
src/main/java/org/asamk/signal/json/JsonSyncMessage.java
Normal file
50
src/main/java/org/asamk/signal/json/JsonSyncMessage.java
Normal file
|
@ -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<String> blockedNumbers;
|
||||
List<ReadMessage> 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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));
|
156
src/main/java/org/asamk/signal/manager/HandleAction.java
Normal file
156
src/main/java/org/asamk/signal/manager/HandleAction.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
29
src/main/java/org/asamk/signal/manager/JsonStickerPack.java
Normal file
29
src/main/java/org/asamk/signal/manager/JsonStickerPack.java
Normal file
|
@ -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<JsonSticker> stickers;
|
||||
|
||||
public static class JsonSticker {
|
||||
|
||||
@JsonProperty
|
||||
public String emoji;
|
||||
|
||||
@JsonProperty
|
||||
public String file;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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) + ")");
|
34
src/main/java/org/asamk/signal/manager/PathConfig.java
Normal file
34
src/main/java/org/asamk/signal/manager/PathConfig.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
117
src/main/java/org/asamk/signal/manager/ProvisioningManager.java
Normal file
117
src/main/java/org/asamk/signal/manager/ProvisioningManager.java
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
80
src/main/java/org/asamk/signal/manager/ServiceConfig.java
Normal file
80
src/main/java/org/asamk/signal/manager/ServiceConfig.java
Normal file
|
@ -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> 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<Interceptor> 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<Integer, SignalCdnUrl[]> makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) {
|
||||
Map<Integer, SignalCdnUrl[]> result = new HashMap<>();
|
||||
result.put(0, cdn0Urls);
|
||||
result.put(2, cdn2Urls);
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
private ServiceConfig() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
public class StickerPackInvalidException extends Exception {
|
||||
|
||||
public StickerPackInvalidException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
public class UserAlreadyExists extends Exception {
|
||||
|
|
@ -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<byte[]> preview = Optional.absent();
|
||||
Optional<String> caption = Optional.absent();
|
||||
Optional<String> blurHash = Optional.absent();
|
||||
return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, blurHash, null);
|
||||
final Optional<ResumableUploadSpec> 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<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients, String localNumber) {
|
||||
Set<SignalServiceAddress> 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<SignalServiceAddress> 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<FileChannel, FileLock> 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<FileChannel, FileLock> 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<FileChannel, FileLock> 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<FileChannel, FileLock> 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<PreKeyRecord> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, ContactInfo> contacts = new HashMap<>();
|
||||
private List<ContactInfo> 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<ContactInfo> 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<Map<?, ?>> {
|
||||
|
||||
@Override
|
||||
public void serialize(final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
|
||||
jgen.writeObject(value.values());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ContactsDeserializer extends JsonDeserializer<Map<String, ContactInfo>> {
|
||||
|
||||
@Override
|
||||
public Map<String, ContactInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
|
||||
Map<String, ContactInfo> contacts = new HashMap<>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
for (JsonNode n : node) {
|
||||
ContactInfo c = jsonProcessor.treeToValue(n, ContactInfo.class);
|
||||
contacts.put(c.number, c);
|
||||
}
|
||||
|
||||
return contacts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,29 @@ package org.asamk.signal.storage.groups;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
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 org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GroupInfo {
|
||||
|
||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
|
||||
@JsonProperty
|
||||
public final byte[] groupId;
|
||||
|
||||
|
@ -18,27 +32,40 @@ public class GroupInfo {
|
|||
public String name;
|
||||
|
||||
@JsonProperty
|
||||
public Set<String> members = new HashSet<>();
|
||||
@JsonProperty
|
||||
public boolean active;
|
||||
@JsonDeserialize(using = MembersDeserializer.class)
|
||||
@JsonSerialize(using = MembersSerializer.class)
|
||||
public Set<SignalServiceAddress> members = new HashSet<>();
|
||||
@JsonProperty
|
||||
public String color;
|
||||
@JsonProperty(defaultValue = "0")
|
||||
public int messageExpirationTime;
|
||||
@JsonProperty(defaultValue = "false")
|
||||
public boolean blocked;
|
||||
@JsonProperty
|
||||
public Integer inboxPosition;
|
||||
@JsonProperty(defaultValue = "false")
|
||||
public boolean archived;
|
||||
|
||||
private long avatarId;
|
||||
|
||||
@JsonProperty
|
||||
@JsonIgnore
|
||||
private boolean active;
|
||||
|
||||
public GroupInfo(byte[] groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<String> members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked) {
|
||||
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<SignalServiceAddress> members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived, @JsonProperty("messageExpirationTime") int messageExpirationTime) {
|
||||
this.groupId = groupId;
|
||||
this.name = name;
|
||||
this.members.addAll(members);
|
||||
this.avatarId = avatarId;
|
||||
this.color = color;
|
||||
this.blocked = blocked;
|
||||
this.inboxPosition = inboxPosition;
|
||||
this.archived = archived;
|
||||
this.messageExpirationTime = messageExpirationTime;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
|
@ -48,16 +75,111 @@ public class GroupInfo {
|
|||
|
||||
@JsonIgnore
|
||||
public Set<SignalServiceAddress> getMembers() {
|
||||
Set<SignalServiceAddress> addresses = new HashSet<>(members.size());
|
||||
for (String member : members) {
|
||||
addresses.add(new SignalServiceAddress(null, member));
|
||||
}
|
||||
return addresses;
|
||||
return members;
|
||||
}
|
||||
|
||||
public void addMembers(Collection<SignalServiceAddress> members) {
|
||||
@JsonIgnore
|
||||
public Set<String> getMembersE164() {
|
||||
Set<String> membersE164 = new HashSet<>();
|
||||
for (SignalServiceAddress member : members) {
|
||||
this.members.add(member.getNumber().get());
|
||||
if (!member.getNumber().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
membersE164.add(member.getNumber().get());
|
||||
}
|
||||
return membersE164;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Set<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
|
||||
Set<SignalServiceAddress> members = new HashSet<>(this.members.size());
|
||||
for (SignalServiceAddress member : this.members) {
|
||||
if (!member.matches(address)) {
|
||||
members.add(member);
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
public void addMembers(Collection<SignalServiceAddress> addresses) {
|
||||
for (SignalServiceAddress address : addresses) {
|
||||
if (this.members.contains(address)) {
|
||||
continue;
|
||||
}
|
||||
removeMember(address);
|
||||
this.members.add(address);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeMember(SignalServiceAddress address) {
|
||||
this.members.removeIf(member -> member.matches(address));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isMember(SignalServiceAddress address) {
|
||||
for (SignalServiceAddress member : this.members) {
|
||||
if (member.matches(address)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class JsonSignalServiceAddress {
|
||||
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
|
||||
this.uuid = uuid;
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
JsonSignalServiceAddress(SignalServiceAddress address) {
|
||||
this.uuid = address.getUuid().orNull();
|
||||
this.number = address.getNumber().orNull();
|
||||
}
|
||||
|
||||
SignalServiceAddress toSignalServiceAddress() {
|
||||
return new SignalServiceAddress(uuid, number);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MembersSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
|
||||
|
||||
@Override
|
||||
public void serialize(final Set<SignalServiceAddress> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
|
||||
jgen.writeStartArray(value.size());
|
||||
for (SignalServiceAddress address : value) {
|
||||
if (address.getUuid().isPresent()) {
|
||||
jgen.writeObject(new JsonSignalServiceAddress(address));
|
||||
} else {
|
||||
jgen.writeString(address.getNumber().get());
|
||||
}
|
||||
}
|
||||
jgen.writeEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static class MembersDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
|
||||
|
||||
@Override
|
||||
public Set<SignalServiceAddress> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
|
||||
Set<SignalServiceAddress> addresses = new HashSet<>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
for (JsonNode n : node) {
|
||||
if (n.isTextual()) {
|
||||
addresses.add(new SignalServiceAddress(null, n.textValue()));
|
||||
} else {
|
||||
JsonSignalServiceAddress address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
|
||||
addresses.add(address.toSignalServiceAddress());
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
package org.asamk.signal.storage.profiles;
|
||||
|
||||
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 org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ProfileStore {
|
||||
|
||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
|
||||
@JsonProperty("profiles")
|
||||
@JsonDeserialize(using = ProfileStoreDeserializer.class)
|
||||
@JsonSerialize(using = ProfileStoreSerializer.class)
|
||||
private final List<SignalProfileEntry> profiles = new ArrayList<>();
|
||||
|
||||
public SignalProfileEntry getProfile(SignalServiceAddress serviceAddress) {
|
||||
for (SignalProfileEntry entry : profiles) {
|
||||
if (entry.getServiceAddress().matches(serviceAddress)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void updateProfile(SignalServiceAddress serviceAddress, ProfileKey profileKey, long now, SignalProfile profile) {
|
||||
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile);
|
||||
for (int i = 0; i < profiles.size(); i++) {
|
||||
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
|
||||
profiles.set(i, newEntry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
profiles.add(newEntry);
|
||||
}
|
||||
|
||||
public static class ProfileStoreDeserializer extends JsonDeserializer<List<SignalProfileEntry>> {
|
||||
|
||||
@Override
|
||||
public List<SignalProfileEntry> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
List<SignalProfileEntry> addresses = new ArrayList<>();
|
||||
|
||||
if (node.isArray()) {
|
||||
for (JsonNode entry : node) {
|
||||
String name = entry.hasNonNull("name")
|
||||
? entry.get("name").asText()
|
||||
: null;
|
||||
UUID uuid = entry.hasNonNull("uuid")
|
||||
? UuidUtil.parseOrNull(entry.get("uuid").asText())
|
||||
: null;
|
||||
final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, name);
|
||||
ProfileKey profileKey = null;
|
||||
try {
|
||||
profileKey = new ProfileKey(Base64.decode(entry.get("profileKey").asText()));
|
||||
} catch (InvalidInputException ignored) {
|
||||
}
|
||||
long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
|
||||
SignalProfile profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
|
||||
addresses.add(new SignalProfileEntry(serviceAddress, profileKey, lastUpdateTimestamp, profile));
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ProfileStoreSerializer extends JsonSerializer<List<SignalProfileEntry>> {
|
||||
|
||||
@Override
|
||||
public void serialize(List<SignalProfileEntry> profiles, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
|
||||
json.writeStartArray();
|
||||
for (SignalProfileEntry profileEntry : profiles) {
|
||||
final SignalServiceAddress address = profileEntry.getServiceAddress();
|
||||
json.writeStartObject();
|
||||
if (address.getNumber().isPresent()) {
|
||||
json.writeStringField("name", address.getNumber().get());
|
||||
}
|
||||
if (address.getUuid().isPresent()) {
|
||||
json.writeStringField("uuid", address.getUuid().get().toString());
|
||||
}
|
||||
json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize()));
|
||||
json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp());
|
||||
json.writeObjectField("profile", profileEntry.getProfile());
|
||||
json.writeEndObject();
|
||||
}
|
||||
json.writeEndArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package org.asamk.signal.storage.profiles;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class SignalProfile {
|
||||
|
||||
@JsonProperty
|
||||
private final String identityKey;
|
||||
|
||||
@JsonProperty
|
||||
private final String name;
|
||||
|
||||
private final File avatarFile;
|
||||
|
||||
@JsonProperty
|
||||
private final String unidentifiedAccess;
|
||||
|
||||
@JsonProperty
|
||||
private final boolean unrestrictedUnidentifiedAccess;
|
||||
|
||||
@JsonProperty
|
||||
private final SignalServiceProfile.Capabilities capabilities;
|
||||
|
||||
public SignalProfile(final String identityKey, final String name, final File avatarFile, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess, final SignalServiceProfile.Capabilities capabilities) {
|
||||
this.identityKey = identityKey;
|
||||
this.name = name;
|
||||
this.avatarFile = avatarFile;
|
||||
this.unidentifiedAccess = unidentifiedAccess;
|
||||
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
|
||||
public SignalProfile(@JsonProperty("identityKey") final String identityKey, @JsonProperty("name") final String name, @JsonProperty("unidentifiedAccess") final String unidentifiedAccess, @JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess, @JsonProperty("capabilities") final SignalServiceProfile.Capabilities capabilities) {
|
||||
this.identityKey = identityKey;
|
||||
this.name = name;
|
||||
this.avatarFile = null;
|
||||
this.unidentifiedAccess = unidentifiedAccess;
|
||||
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
|
||||
public String getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public File getAvatarFile() {
|
||||
return avatarFile;
|
||||
}
|
||||
|
||||
public String getUnidentifiedAccess() {
|
||||
return unidentifiedAccess;
|
||||
}
|
||||
|
||||
public boolean isUnrestrictedUnidentifiedAccess() {
|
||||
return unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
|
||||
public SignalServiceProfile.Capabilities getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SignalProfile{" +
|
||||
"identityKey='" + identityKey + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", avatarFile=" + avatarFile +
|
||||
", unidentifiedAccess='" + unidentifiedAccess + '\'' +
|
||||
", unrestrictedUnidentifiedAccess=" + unrestrictedUnidentifiedAccess +
|
||||
", capabilities=" + capabilities +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package org.asamk.signal.storage.profiles;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public class SignalProfileEntry {
|
||||
|
||||
private final SignalServiceAddress serviceAddress;
|
||||
|
||||
private final ProfileKey profileKey;
|
||||
|
||||
private final long lastUpdateTimestamp;
|
||||
|
||||
private final SignalProfile profile;
|
||||
|
||||
public SignalProfileEntry(final SignalServiceAddress serviceAddress, final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile) {
|
||||
this.serviceAddress = serviceAddress;
|
||||
this.profileKey = profileKey;
|
||||
this.lastUpdateTimestamp = lastUpdateTimestamp;
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
public SignalServiceAddress getServiceAddress() {
|
||||
return serviceAddress;
|
||||
}
|
||||
|
||||
public ProfileKey getProfileKey() {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public long getLastUpdateTimestamp() {
|
||||
return lastUpdateTimestamp;
|
||||
}
|
||||
|
||||
public SignalProfile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
}
|
|
@ -8,33 +8,49 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import org.asamk.signal.TrustLevel;
|
||||
import org.asamk.signal.manager.TrustLevel;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.state.IdentityKeyStore;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class JsonIdentityKeyStore implements IdentityKeyStore {
|
||||
|
||||
private final Map<String, List<Identity>> trustedKeys = new HashMap<>();
|
||||
private final List<Identity> identities = new ArrayList<>();
|
||||
|
||||
private final IdentityKeyPair identityKeyPair;
|
||||
private final int localRegistrationId;
|
||||
|
||||
private SignalServiceAddressResolver resolver;
|
||||
|
||||
public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) {
|
||||
this.identityKeyPair = identityKeyPair;
|
||||
this.localRegistrationId = localRegistrationId;
|
||||
}
|
||||
|
||||
public void setResolver(final SignalServiceAddressResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
|
||||
if (resolver != null) {
|
||||
return resolver.resolveSignalServiceAddress(identifier);
|
||||
} else {
|
||||
return Util.getSignalServiceAddressFromIdentifier(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKeyPair getIdentityKeyPair() {
|
||||
return identityKeyPair;
|
||||
|
@ -47,85 +63,116 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return saveIdentity(address.getName(), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null);
|
||||
return saveIdentity(resolveSignalServiceAddress(address.getName()), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates the given identityKey for the user name and sets the trustLevel and added timestamp.
|
||||
* Adds the given identityKey for the user name and sets the trustLevel and added timestamp.
|
||||
* If the identityKey already exists, the trustLevel and added timestamp are NOT updated.
|
||||
*
|
||||
* @param name User name, i.e. phone number
|
||||
* @param identityKey The user's public key
|
||||
* @param trustLevel
|
||||
* @param added Added timestamp, if null and the key is newly added, the current time is used.
|
||||
* @param serviceAddress User address, i.e. phone number and/or uuid
|
||||
* @param identityKey The user's public key
|
||||
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
|
||||
* @param added Added timestamp, if null and the key is newly added, the current time is used.
|
||||
*/
|
||||
public boolean saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
|
||||
List<Identity> identities = trustedKeys.get(name);
|
||||
if (identities == null) {
|
||||
identities = new ArrayList<>();
|
||||
trustedKeys.put(name, identities);
|
||||
} else {
|
||||
for (Identity id : identities) {
|
||||
if (!id.identityKey.equals(identityKey))
|
||||
continue;
|
||||
|
||||
if (id.trustLevel.compareTo(trustLevel) < 0) {
|
||||
id.trustLevel = trustLevel;
|
||||
}
|
||||
if (added != null) {
|
||||
id.added = added;
|
||||
}
|
||||
return true;
|
||||
public boolean saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
|
||||
for (Identity id : identities) {
|
||||
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
|
||||
id.address = serviceAddress;
|
||||
}
|
||||
// Identity already exists, not updating the trust level
|
||||
return true;
|
||||
}
|
||||
identities.add(new Identity(identityKey, trustLevel, added != null ? added : new Date()));
|
||||
|
||||
identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update trustLevel for the given identityKey for the user name.
|
||||
*
|
||||
* @param serviceAddress User address, i.e. phone number and/or uuid
|
||||
* @param identityKey The user's public key
|
||||
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
|
||||
*/
|
||||
public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
|
||||
for (Identity id : identities) {
|
||||
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
|
||||
id.address = serviceAddress;
|
||||
}
|
||||
id.trustLevel = trustLevel;
|
||||
return;
|
||||
}
|
||||
|
||||
identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||
// TODO implement possibility for different handling of incoming/outgoing trust decisions
|
||||
List<Identity> identities = trustedKeys.get(address.getName());
|
||||
if (identities == null) {
|
||||
// Trust on first use
|
||||
return true;
|
||||
}
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
|
||||
boolean trustOnFirstUse = true;
|
||||
|
||||
for (Identity id : identities) {
|
||||
if (!id.address.matches(serviceAddress)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (id.identityKey.equals(identityKey)) {
|
||||
return id.isTrusted();
|
||||
} else {
|
||||
trustOnFirstUse = false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return trustOnFirstUse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey getIdentity(SignalProtocolAddress address) {
|
||||
List<Identity> identities = trustedKeys.get(address.getName());
|
||||
if (identities == null || identities.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
|
||||
Identity identity = getIdentity(serviceAddress);
|
||||
return identity == null ? null : identity.getIdentityKey();
|
||||
}
|
||||
|
||||
public Identity getIdentity(SignalServiceAddress serviceAddress) {
|
||||
long maxDate = 0;
|
||||
Identity maxIdentity = null;
|
||||
for (Identity id : identities) {
|
||||
for (Identity id : this.identities) {
|
||||
if (!id.address.matches(serviceAddress)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final long time = id.getDateAdded().getTime();
|
||||
if (maxIdentity == null || maxDate <= time) {
|
||||
maxDate = time;
|
||||
maxIdentity = id;
|
||||
}
|
||||
}
|
||||
return maxIdentity.getIdentityKey();
|
||||
return maxIdentity;
|
||||
}
|
||||
|
||||
public Map<String, List<Identity>> getIdentities() {
|
||||
public List<Identity> getIdentities() {
|
||||
// TODO deep copy
|
||||
return trustedKeys;
|
||||
return identities;
|
||||
}
|
||||
|
||||
public List<Identity> getIdentities(String name) {
|
||||
// TODO deep copy
|
||||
return trustedKeys.get(name);
|
||||
public List<Identity> getIdentities(SignalServiceAddress serviceAddress) {
|
||||
List<Identity> identities = new ArrayList<>();
|
||||
for (Identity identity : this.identities) {
|
||||
if (identity.address.matches(serviceAddress)) {
|
||||
identities.add(identity);
|
||||
}
|
||||
}
|
||||
return identities;
|
||||
}
|
||||
|
||||
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
|
||||
|
@ -143,12 +190,26 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
JsonNode trustedKeysNode = node.get("trustedKeys");
|
||||
if (trustedKeysNode.isArray()) {
|
||||
for (JsonNode trustedKey : trustedKeysNode) {
|
||||
String trustedKeyName = trustedKey.get("name").asText();
|
||||
String trustedKeyName = trustedKey.hasNonNull("name")
|
||||
? trustedKey.get("name").asText()
|
||||
: null;
|
||||
|
||||
if (UuidUtil.isUuid(trustedKeyName)) {
|
||||
// Ignore identities that were incorrectly created with UUIDs as name
|
||||
continue;
|
||||
}
|
||||
|
||||
UUID uuid = trustedKey.hasNonNull("uuid")
|
||||
? UuidUtil.parseOrNull(trustedKey.get("uuid").asText())
|
||||
: null;
|
||||
final SignalServiceAddress serviceAddress = uuid == null
|
||||
? Util.getSignalServiceAddressFromIdentifier(trustedKeyName)
|
||||
: new SignalServiceAddress(uuid, trustedKeyName);
|
||||
try {
|
||||
IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0);
|
||||
TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get("trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
|
||||
Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp").asLong()) : new Date();
|
||||
keyStore.saveIdentity(trustedKeyName, id, trustLevel, added);
|
||||
keyStore.saveIdentity(serviceAddress, id, trustLevel, added);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
System.out.println(String.format("Error while decoding key for: %s", trustedKeyName));
|
||||
}
|
||||
|
@ -170,39 +231,53 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId());
|
||||
json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
|
||||
json.writeArrayFieldStart("trustedKeys");
|
||||
for (Map.Entry<String, List<Identity>> trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) {
|
||||
for (Identity id : trustedKey.getValue()) {
|
||||
json.writeStartObject();
|
||||
json.writeStringField("name", trustedKey.getKey());
|
||||
json.writeStringField("identityKey", Base64.encodeBytes(id.identityKey.serialize()));
|
||||
json.writeNumberField("trustLevel", id.trustLevel.ordinal());
|
||||
json.writeNumberField("addedTimestamp", id.added.getTime());
|
||||
json.writeEndObject();
|
||||
for (Identity trustedKey : jsonIdentityKeyStore.identities) {
|
||||
json.writeStartObject();
|
||||
if (trustedKey.getAddress().getNumber().isPresent()) {
|
||||
json.writeStringField("name", trustedKey.getAddress().getNumber().get());
|
||||
}
|
||||
if (trustedKey.getAddress().getUuid().isPresent()) {
|
||||
json.writeStringField("uuid", trustedKey.getAddress().getUuid().get().toString());
|
||||
}
|
||||
json.writeStringField("identityKey", Base64.encodeBytes(trustedKey.identityKey.serialize()));
|
||||
json.writeNumberField("trustLevel", trustedKey.trustLevel.ordinal());
|
||||
json.writeNumberField("addedTimestamp", trustedKey.added.getTime());
|
||||
json.writeEndObject();
|
||||
}
|
||||
json.writeEndArray();
|
||||
json.writeEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
public class Identity {
|
||||
public static class Identity {
|
||||
|
||||
SignalServiceAddress address;
|
||||
IdentityKey identityKey;
|
||||
TrustLevel trustLevel;
|
||||
Date added;
|
||||
|
||||
public Identity(IdentityKey identityKey, TrustLevel trustLevel) {
|
||||
public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) {
|
||||
this.address = address;
|
||||
this.identityKey = identityKey;
|
||||
this.trustLevel = trustLevel;
|
||||
this.added = new Date();
|
||||
}
|
||||
|
||||
Identity(IdentityKey identityKey, TrustLevel trustLevel, Date added) {
|
||||
Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
|
||||
this.address = address;
|
||||
this.identityKey = identityKey;
|
||||
this.trustLevel = trustLevel;
|
||||
this.added = added;
|
||||
}
|
||||
|
||||
public SignalServiceAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(final SignalServiceAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
boolean isTrusted() {
|
||||
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED ||
|
||||
trustLevel == TrustLevel.TRUSTED_VERIFIED;
|
||||
|
|
|
@ -70,7 +70,7 @@ class JsonPreKeyStore implements PreKeyStore {
|
|||
try {
|
||||
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
|
||||
} catch (IOException e) {
|
||||
System.out.println(String.format("Error while decoding prekey for: %s", preKeyId));
|
||||
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,51 +8,72 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.libsignal.state.SessionStore;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
class JsonSessionStore implements SessionStore {
|
||||
|
||||
private final Map<SignalProtocolAddress, byte[]> sessions = new HashMap<>();
|
||||
private final List<SessionInfo> sessions = new ArrayList<>();
|
||||
|
||||
private SignalServiceAddressResolver resolver;
|
||||
|
||||
public JsonSessionStore() {
|
||||
|
||||
}
|
||||
|
||||
private void addSessions(Map<SignalProtocolAddress, byte[]> sessions) {
|
||||
this.sessions.putAll(sessions);
|
||||
public void setResolver(final SignalServiceAddressResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) {
|
||||
try {
|
||||
if (containsSession(remoteAddress)) {
|
||||
return new SessionRecord(sessions.get(remoteAddress));
|
||||
} else {
|
||||
return new SessionRecord();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
|
||||
if (resolver != null) {
|
||||
return resolver.resolveSignalServiceAddress(identifier);
|
||||
} else {
|
||||
return Util.getSignalServiceAddressFromIdentifier(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized List<Integer> getSubDeviceSessions(String name) {
|
||||
List<Integer> deviceIds = new LinkedList<>();
|
||||
public synchronized SessionRecord loadSession(SignalProtocolAddress address) {
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
|
||||
for (SessionInfo info : sessions) {
|
||||
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
|
||||
try {
|
||||
return new SessionRecord(info.sessionRecord);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to load session, resetting session: " + e);
|
||||
final SessionRecord sessionRecord = new SessionRecord();
|
||||
info.sessionRecord = sessionRecord.serialize();
|
||||
return sessionRecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (SignalProtocolAddress key : sessions.keySet()) {
|
||||
if (key.getName().equals(name) &&
|
||||
key.getDeviceId() != 1) {
|
||||
deviceIds.add(key.getDeviceId());
|
||||
return new SessionRecord();
|
||||
}
|
||||
|
||||
public synchronized List<SessionInfo> getSessions() {
|
||||
return sessions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized List<Integer> getSubDeviceSessions(String name) {
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
|
||||
|
||||
List<Integer> deviceIds = new LinkedList<>();
|
||||
for (SessionInfo info : sessions) {
|
||||
if (info.address.matches(serviceAddress) && info.deviceId != 1) {
|
||||
deviceIds.add(info.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,26 +82,45 @@ class JsonSessionStore implements SessionStore {
|
|||
|
||||
@Override
|
||||
public synchronized void storeSession(SignalProtocolAddress address, SessionRecord record) {
|
||||
sessions.put(address, record.serialize());
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
|
||||
for (SessionInfo info : sessions) {
|
||||
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
|
||||
if (!info.address.getUuid().isPresent() || !info.address.getNumber().isPresent()) {
|
||||
info.address = serviceAddress;
|
||||
}
|
||||
info.sessionRecord = record.serialize();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sessions.add(new SessionInfo(serviceAddress, address.getDeviceId(), record.serialize()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean containsSession(SignalProtocolAddress address) {
|
||||
return sessions.containsKey(address);
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
|
||||
for (SessionInfo info : sessions) {
|
||||
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void deleteSession(SignalProtocolAddress address) {
|
||||
sessions.remove(address);
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
|
||||
sessions.removeIf(info -> info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void deleteAllSessions(String name) {
|
||||
for (SignalProtocolAddress key : new ArrayList<>(sessions.keySet())) {
|
||||
if (key.getName().equals(name)) {
|
||||
sessions.remove(key);
|
||||
}
|
||||
}
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
|
||||
deleteAllSessions(serviceAddress);
|
||||
}
|
||||
|
||||
public synchronized void deleteAllSessions(SignalServiceAddress serviceAddress) {
|
||||
sessions.removeIf(info -> info.address.matches(serviceAddress));
|
||||
}
|
||||
|
||||
public static class JsonSessionStoreDeserializer extends JsonDeserializer<JsonSessionStore> {
|
||||
|
@ -89,39 +129,58 @@ class JsonSessionStore implements SessionStore {
|
|||
public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
Map<SignalProtocolAddress, byte[]> sessionMap = new HashMap<>();
|
||||
JsonSessionStore sessionStore = new JsonSessionStore();
|
||||
|
||||
if (node.isArray()) {
|
||||
for (JsonNode session : node) {
|
||||
String sessionName = session.get("name").asText();
|
||||
String sessionName = session.hasNonNull("name")
|
||||
? session.get("name").asText()
|
||||
: null;
|
||||
if (UuidUtil.isUuid(sessionName)) {
|
||||
// Ignore sessions that were incorrectly created with UUIDs as name
|
||||
continue;
|
||||
}
|
||||
|
||||
UUID uuid = session.hasNonNull("uuid")
|
||||
? UuidUtil.parseOrNull(session.get("uuid").asText())
|
||||
: null;
|
||||
final SignalServiceAddress serviceAddress = uuid == null
|
||||
? Util.getSignalServiceAddressFromIdentifier(sessionName)
|
||||
: new SignalServiceAddress(uuid, sessionName);
|
||||
final int deviceId = session.get("deviceId").asInt();
|
||||
final String record = session.get("record").asText();
|
||||
try {
|
||||
sessionMap.put(new SignalProtocolAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText()));
|
||||
SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record));
|
||||
sessionStore.sessions.add(sessionInfo);
|
||||
} catch (IOException e) {
|
||||
System.out.println(String.format("Error while decoding session for: %s", sessionName));
|
||||
System.err.println(String.format("Error while decoding session for: %s", sessionName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JsonSessionStore sessionStore = new JsonSessionStore();
|
||||
sessionStore.addSessions(sessionMap);
|
||||
|
||||
return sessionStore;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static class JsonPreKeyStoreSerializer extends JsonSerializer<JsonSessionStore> {
|
||||
public static class JsonSessionStoreSerializer extends JsonSerializer<JsonSessionStore> {
|
||||
|
||||
@Override
|
||||
public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
|
||||
json.writeStartArray();
|
||||
for (Map.Entry<SignalProtocolAddress, byte[]> preKey : jsonSessionStore.sessions.entrySet()) {
|
||||
for (SessionInfo sessionInfo : jsonSessionStore.sessions) {
|
||||
json.writeStartObject();
|
||||
json.writeStringField("name", preKey.getKey().getName());
|
||||
json.writeNumberField("deviceId", preKey.getKey().getDeviceId());
|
||||
json.writeStringField("record", Base64.encodeBytes(preKey.getValue()));
|
||||
if (sessionInfo.address.getNumber().isPresent()) {
|
||||
json.writeStringField("name", sessionInfo.address.getNumber().get());
|
||||
}
|
||||
if (sessionInfo.address.getUuid().isPresent()) {
|
||||
json.writeStringField("uuid", sessionInfo.address.getUuid().get().toString());
|
||||
}
|
||||
json.writeNumberField("deviceId", sessionInfo.deviceId);
|
||||
json.writeStringField("record", Base64.encodeBytes(sessionInfo.sessionRecord));
|
||||
json.writeEndObject();
|
||||
}
|
||||
json.writeEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.asamk.signal.TrustLevel;
|
||||
import org.asamk.signal.manager.TrustLevel;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
|
@ -13,9 +13,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
|
|||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class JsonSignalProtocolStore implements SignalProtocolStore {
|
||||
|
||||
|
@ -26,7 +26,7 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
|
|||
|
||||
@JsonProperty("sessionStore")
|
||||
@JsonDeserialize(using = JsonSessionStore.JsonSessionStoreDeserializer.class)
|
||||
@JsonSerialize(using = JsonSessionStore.JsonPreKeyStoreSerializer.class)
|
||||
@JsonSerialize(using = JsonSessionStore.JsonSessionStoreSerializer.class)
|
||||
private JsonSessionStore sessionStore;
|
||||
|
||||
@JsonProperty("signedPreKeyStore")
|
||||
|
@ -56,6 +56,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
|
|||
this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
|
||||
}
|
||||
|
||||
public void setResolver(final SignalServiceAddressResolver resolver) {
|
||||
sessionStore.setResolver(resolver);
|
||||
identityKeyStore.setResolver(resolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKeyPair getIdentityKeyPair() {
|
||||
return identityKeyStore.getIdentityKeyPair();
|
||||
|
@ -71,16 +76,20 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
|
|||
return identityKeyStore.saveIdentity(address, identityKey);
|
||||
}
|
||||
|
||||
public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) {
|
||||
identityKeyStore.saveIdentity(name, identityKey, trustLevel, null);
|
||||
public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
|
||||
identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null);
|
||||
}
|
||||
|
||||
public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
|
||||
public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
|
||||
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
|
||||
}
|
||||
|
||||
public List<JsonIdentityKeyStore.Identity> getIdentities() {
|
||||
return identityKeyStore.getIdentities();
|
||||
}
|
||||
|
||||
public List<JsonIdentityKeyStore.Identity> getIdentities(String name) {
|
||||
return identityKeyStore.getIdentities(name);
|
||||
public List<JsonIdentityKeyStore.Identity> getIdentities(SignalServiceAddress serviceAddress) {
|
||||
return identityKeyStore.getIdentities(serviceAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -93,6 +102,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
|
|||
return identityKeyStore.getIdentity(address);
|
||||
}
|
||||
|
||||
public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) {
|
||||
return identityKeyStore.getIdentity(serviceAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
||||
return preKeyStore.loadPreKey(preKeyId);
|
||||
|
@ -118,6 +131,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
|
|||
return sessionStore.loadSession(address);
|
||||
}
|
||||
|
||||
public List<SessionInfo> getSessions() {
|
||||
return sessionStore.getSessions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getSubDeviceSessions(String name) {
|
||||
return sessionStore.getSubDeviceSessions(name);
|
||||
|
@ -143,6 +160,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
|
|||
sessionStore.deleteAllSessions(name);
|
||||
}
|
||||
|
||||
public void deleteAllSessions(SignalServiceAddress serviceAddress) {
|
||||
sessionStore.deleteAllSessions(serviceAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
|
||||
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId);
|
||||
|
|
|
@ -87,7 +87,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
|
|||
try {
|
||||
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
|
||||
} catch (IOException e) {
|
||||
System.out.println(String.format("Error while decoding prekey for: %s", preKeyId));
|
||||
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
|
||||
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.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RecipientStore {
|
||||
|
||||
@JsonProperty("recipientStore")
|
||||
@JsonDeserialize(using = RecipientStoreDeserializer.class)
|
||||
@JsonSerialize(using = RecipientStoreSerializer.class)
|
||||
private final Set<SignalServiceAddress> addresses = new HashSet<>();
|
||||
|
||||
public SignalServiceAddress resolveServiceAddress(SignalServiceAddress serviceAddress) {
|
||||
if (addresses.contains(serviceAddress)) {
|
||||
// If the Set already contains the exact address with UUID and Number,
|
||||
// we can just return it here.
|
||||
return serviceAddress;
|
||||
}
|
||||
|
||||
for (SignalServiceAddress address : addresses) {
|
||||
if (address.matches(serviceAddress)) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceAddress.getNumber().isPresent() && serviceAddress.getUuid().isPresent()) {
|
||||
addresses.add(serviceAddress);
|
||||
}
|
||||
|
||||
return serviceAddress;
|
||||
}
|
||||
|
||||
public static class RecipientStoreDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
|
||||
|
||||
@Override
|
||||
public Set<SignalServiceAddress> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
Set<SignalServiceAddress> addresses = new HashSet<>();
|
||||
|
||||
if (node.isArray()) {
|
||||
for (JsonNode recipient : node) {
|
||||
String recipientName = recipient.get("name").asText();
|
||||
UUID uuid = UuidUtil.parseOrThrow(recipient.get("uuid").asText());
|
||||
final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, recipientName);
|
||||
addresses.add(serviceAddress);
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RecipientStoreSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
|
||||
|
||||
@Override
|
||||
public void serialize(Set<SignalServiceAddress> addresses, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
|
||||
json.writeStartArray();
|
||||
for (SignalServiceAddress address : addresses) {
|
||||
json.writeStartObject();
|
||||
json.writeStringField("name", address.getNumber().get());
|
||||
json.writeStringField("uuid", address.getUuid().get().toString());
|
||||
json.writeEndObject();
|
||||
}
|
||||
json.writeEndArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public class SessionInfo {
|
||||
|
||||
public SignalServiceAddress address;
|
||||
|
||||
public int deviceId;
|
||||
|
||||
public byte[] sessionRecord;
|
||||
|
||||
public SessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) {
|
||||
this.address = address;
|
||||
this.deviceId = deviceId;
|
||||
this.sessionRecord = sessionRecord;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public interface SignalServiceAddressResolver {
|
||||
|
||||
/**
|
||||
* Get a SignalServiceAddress with number and/or uuid from an identifier name.
|
||||
*
|
||||
* @param identifier can be either a serialized uuid or a e164 phone number
|
||||
*/
|
||||
SignalServiceAddress resolveSignalServiceAddress(String identifier);
|
||||
}
|
|
@ -18,23 +18,15 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class JsonThreadStore {
|
||||
public class LegacyJsonThreadStore {
|
||||
|
||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
|
||||
@JsonProperty("threads")
|
||||
@JsonSerialize(using = JsonThreadStore.MapToListSerializer.class)
|
||||
@JsonSerialize(using = MapToListSerializer.class)
|
||||
@JsonDeserialize(using = ThreadsDeserializer.class)
|
||||
private Map<String, ThreadInfo> threads = new HashMap<>();
|
||||
|
||||
public void updateThread(ThreadInfo thread) {
|
||||
threads.put(thread.id, thread);
|
||||
}
|
||||
|
||||
public ThreadInfo getThread(String id) {
|
||||
return threads.get(id);
|
||||
}
|
||||
|
||||
public List<ThreadInfo> getThreads() {
|
||||
return new ArrayList<>(threads.values());
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
package org.asamk.signal.util;
|
||||
|
||||
import org.asamk.signal.GroupIdFormatException;
|
||||
import org.asamk.signal.GroupNotFoundException;
|
||||
import org.asamk.signal.NotAGroupMemberException;
|
||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||
import org.asamk.signal.manager.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.NotAGroupMemberException;
|
||||
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;
|
||||
|
||||
|
@ -50,13 +49,13 @@ public class ErrorUtils {
|
|||
System.err.println("Aborting sending.");
|
||||
}
|
||||
|
||||
public static void handleDBusExecutionException(DBusExecutionException e) {
|
||||
System.err.println("Cannot connect to dbus: " + e.getMessage());
|
||||
System.err.println("Aborting.");
|
||||
}
|
||||
|
||||
public static void handleGroupIdFormatException(GroupIdFormatException e) {
|
||||
System.err.println(e.getMessage());
|
||||
System.err.println("Aborting sending.");
|
||||
}
|
||||
|
||||
public static void handleInvalidNumberException(InvalidNumberException e) {
|
||||
System.err.println("Failed to parse recipient: " + e.getMessage());
|
||||
System.err.println("Aborting sending.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal;
|
||||
package org.asamk.signal.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -9,6 +9,15 @@ public class Hex {
|
|||
private Hex() {
|
||||
}
|
||||
|
||||
public static String toString(byte[] bytes) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
for (final byte aByte : bytes) {
|
||||
appendHexChar(buf, aByte);
|
||||
buf.append(" ");
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
public static String toStringCondensed(byte[] bytes) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
for (final byte aByte : bytes) {
|
||||
|
@ -20,7 +29,6 @@ public class Hex {
|
|||
private static void appendHexChar(StringBuffer buf, int b) {
|
||||
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
|
||||
buf.append(HEX_DIGITS[b & 0xf]);
|
||||
buf.append(" ");
|
||||
}
|
||||
|
||||
public static byte[] toByteArray(String s) {
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
package org.asamk.signal.util;
|
||||
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
|
@ -35,6 +40,12 @@ public class IOUtils {
|
|||
return output.toString();
|
||||
}
|
||||
|
||||
public static byte[] readFully(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Util.copy(in, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static void createPrivateDirectories(String directoryPath) throws IOException {
|
||||
final File file = new File(directoryPath);
|
||||
if (file.exists()) {
|
||||
|
@ -68,4 +79,19 @@ public class IOUtils {
|
|||
|
||||
return System.getProperty("user.home") + "/.local/share";
|
||||
}
|
||||
|
||||
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException {
|
||||
copyStreamToFile(input, outputFile, 8192);
|
||||
}
|
||||
|
||||
public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException {
|
||||
try (OutputStream output = new FileOutputStream(outputFile)) {
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int read;
|
||||
|
||||
while ((read = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,14 @@ import java.security.SecureRandom;
|
|||
|
||||
public class RandomUtils {
|
||||
|
||||
private static final ThreadLocal<SecureRandom> LOCAL_RANDOM = new ThreadLocal<SecureRandom>() {
|
||||
@Override
|
||||
protected SecureRandom initialValue() {
|
||||
SecureRandom rand = getSecureRandomUnseeded();
|
||||
private static final ThreadLocal<SecureRandom> LOCAL_RANDOM = ThreadLocal.withInitial(() -> {
|
||||
SecureRandom rand = getSecureRandomUnseeded();
|
||||
|
||||
// Let the SecureRandom seed it self initially
|
||||
rand.nextBoolean();
|
||||
// Let the SecureRandom seed it self initially
|
||||
rand.nextBoolean();
|
||||
|
||||
return rand;
|
||||
}
|
||||
};
|
||||
return rand;
|
||||
});
|
||||
|
||||
private static SecureRandom getSecureRandomUnseeded() {
|
||||
try {
|
||||
|
|
|
@ -11,15 +11,15 @@ public class SecurityProvider extends Provider {
|
|||
private static final String info = "Security Provider v1.0";
|
||||
|
||||
public SecurityProvider() {
|
||||
super(PROVIDER_NAME, 1.0, info);
|
||||
super(PROVIDER_NAME, "1.0", info);
|
||||
put("SecureRandom.DEFAULT", DefaultRandom.class.getName());
|
||||
|
||||
// Workaround for BKS truststore
|
||||
put("KeyStore.BKS", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Std");
|
||||
put("KeyStore.BKS-V1", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Version1");
|
||||
put("KeyStore.BouncyCastle", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$BouncyCastleStore");
|
||||
put("KeyFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory");
|
||||
put("CertificateFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory");
|
||||
put("KeyStore.BKS", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Std.class.getCanonicalName());
|
||||
put("KeyStore.BKS-V1", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Version1.class.getCanonicalName());
|
||||
put("KeyStore.BouncyCastle", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.BouncyCastleStore.class.getCanonicalName());
|
||||
put("KeyFactory.X.509", org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory.class.getCanonicalName());
|
||||
put("CertificateFactory.X.509", org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory.class.getCanonicalName());
|
||||
}
|
||||
|
||||
public static class DefaultRandom extends SecureRandomSpi {
|
||||
|
|
|
@ -2,7 +2,10 @@ package org.asamk.signal.util;
|
|||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import org.asamk.signal.GroupIdFormatException;
|
||||
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.UuidUtil;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -51,4 +54,16 @@ public class Util {
|
|||
throw new GroupIdFormatException(groupId, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {
|
||||
return PhoneNumberFormatter.formatNumber(number, localNumber);
|
||||
}
|
||||
|
||||
public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) {
|
||||
if (UuidUtil.isUuid(identifier)) {
|
||||
return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null);
|
||||
} else {
|
||||
return new SignalServiceAddress(null, identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue