Compare commits

..

No commits in common. "master" and "v0.12.2" have entirely different histories.

418 changed files with 10193 additions and 37203 deletions

View File

@ -1,57 +0,0 @@
name: build
on:
push:
branches:
- "**"
pull_request:
workflow_call:
permissions: {}
jobs:
build:
strategy:
matrix:
# java="25" is the LTS Java version used in reproducible builds script (default in Containerfile).
# More Java versions can be added to test compatibility, eg. "26".
java: ["25", "26"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build
run: |
if [ "${{ matrix.java }}" != "25" ]; then
export OVERRIDE_JAVA_VERSION="${{ matrix.java }}"
fi
./reproducible-builds/build.sh
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-archive-${{ matrix.java }}
path: dist/*
build-client:
strategy:
matrix:
os:
- ubuntu
- macos
- windows
runs-on: ${{ matrix.os }}-latest
defaults:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v6
- name: Install rust
run: rustup default stable
- name: Build client
run: cargo build --release --verbose
- name: Archive production artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-client-${{ matrix.os }}
path: |
client/target/release/signal-cli-client
client/target/release/signal-cli-client.exe

58
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: signal-cli CI
on:
push:
branches:
- '**'
pull_request:
workflow_call:
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17', '21' ]
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
cache: 'gradle'
- name: Build with Gradle
run: ./gradlew --no-daemon build
- name: Compress archive
run: gzip -n -9 build/distributions/signal-cli-*.tar
- name: Archive production artifacts
uses: actions/upload-artifact@v3
with:
name: signal-cli-archive-${{ matrix.java }}
path: build/distributions/signal-cli-*.tar.gz
build-graalvm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: graalvm/setup-graalvm@v1
with:
version: 'latest'
java-version: '17'
cache: 'gradle'
components: 'native-image'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build with Gradle
run: ./gradlew --no-daemon nativeCompile
- name: Archive production artifacts
uses: actions/upload-artifact@v3
with:
name: signal-cli-native
path: build/native/nativeCompile/signal-cli

View File

@ -21,13 +21,13 @@ jobs:
steps: steps:
- name: Setup Java JDK - name: Setup Java JDK
uses: actions/setup-java@v5 uses: actions/setup-java@v3
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 25 java-version: 17
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v3
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@ -35,7 +35,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v2
# Override language selection by uncommenting this and choosing your languages # Override language selection by uncommenting this and choosing your languages
# with: # with:
# languages: go, javascript, csharp, python, cpp, java # languages: go, javascript, csharp, python, cpp, java
@ -43,7 +43,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v4 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4 uses: github/codeql-action/analyze@v2

View File

@ -1,27 +0,0 @@
name: dependency-graph
on:
push:
branches:
- "**"
permissions: {}
jobs:
submit-dependency-graph:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '25'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
dependency-graph: generate-and-submit
- name: Generate dependency graph
run: ./gradlew --no-daemon dependencies

View File

@ -5,78 +5,185 @@ on:
tags: tags:
- v* - v*
permissions: {} permissions:
contents: read # to fetch code (actions/checkout)
env: env:
IMAGE_NAME: signal-cli IMAGE_NAME: signal-cli
IMAGE_REGISTRY: ghcr.io/asamk IMAGE_REGISTRY: ghcr.io/asamk
REGISTRY_USER: ${{ github.actor }} REGISTRY_USER: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ github.token }} REGISTRY_PASSWORD: ${{ github.token }}
ARCHIVE_JAVA_VERSION: 25
jobs: jobs:
build:
uses: ./.github/workflows/build.yml
release: ci_wf:
needs: build uses: AsamK/signal-cli/.github/workflows/ci.yml@master
# ${{ github.repository }} not accepted here
lib_to_jar:
needs: ci_wf
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
outputs: outputs:
version: ${{ steps.version.outputs.version }} signal_cli_version: ${{ steps.cli_ver.outputs.version }}
release_id: ${{ steps.create_release.outputs.id }}
steps: steps:
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8 uses: actions/download-artifact@v3
- name: Get signal-cli version - name: Get signal-cli version
id: version id: cli_ver
run: | run: |
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* . ver="${GITHUB_REF_NAME#v}"
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Create draft release and upload assets - name: Extract archive
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
gh release create "v${{ steps.version.outputs.version }}" \ tree .
--repo "${{ github.repository }}" \ ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
--title "v${{ steps.version.outputs.version }}" \ tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
--draft \ mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
--verify-tag \ rm -rf signal-cli-archive-*/
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
"signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \ - name: Get signal-client jar version
"signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \ id: lib_ver
"signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz" run: |
JAR_PREFIX=libsignal-client-
jar_file=$(find ./signal-cli-*/lib/ -name "$JAR_PREFIX*.jar")
jar_version=$(echo "$jar_file" | xargs basename | sed "s/$JAR_PREFIX//; s/.jar//")
echo "$jar_version"
echo "signal_client_version=${jar_version}" >> $GITHUB_OUTPUT
- name: Download signal-client builds
env:
RELEASES_URL: https://github.com/signalapp/libsignal/releases/download/
FILE_NAMES: signal_jni.dll libsignal_jni.dylib
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
run: |
for file_name in $FILE_NAMES; do
curl -sOL "${RELEASES_URL}/v${SIGNAL_CLIENT_VER}/${file_name}" # note: added v
done
tree .
- name: Compress native app
env:
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
run: |
chmod +x signal-cli-native/signal-cli
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-native.tar.gz -C signal-cli-native signal-cli
rm -rf signal-cli-native/
- name: Replace Windows lib
env:
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
run: |
mv signal_jni.dll libsignal_jni.so
zip -u ./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar ./libsignal_jni.so
tar -czf signal-cli-${SIGNAL_CLI_VER}-Windows.tar.gz signal-cli-*/
- name: Replace macOS lib
env:
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
run: |
jar_file=./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar
zip -d $jar_file libsignal_jni.so
zip $jar_file libsignal_jni.dylib
tar -czf signal-cli-${SIGNAL_CLI_VER}-macOS.tar.gz signal-cli-*/
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
release_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
draft: true
- name: Upload Linux archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
- name: Upload Linux native archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
- name: Upload windows archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
- name: Upload macos archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
build-container: build-container:
needs: release needs: ci_wf
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read
packages: write packages: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8 uses: actions/download-artifact@v3
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file - name: Move archive file
run: | run: |
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}.tar.gz ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
rm -r signal-cli-archive-* signal-cli-native
mkdir -p build/install/ mkdir -p build/install/
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli mv ./signal-cli-*/ build/install/signal-cli
- name: Build Image - name: Build Image
id: build_image id: build_image
uses: redhat-actions/buildah-build@v3 uses: redhat-actions/buildah-build@v2
with: with:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }} tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
containerfiles: ./Containerfile containerfiles:
./Containerfile
oci: true oci: true
- name: Push To GHCR - name: Push To GHCR
uses: redhat-actions/push-to-registry@v3 uses: redhat-actions/push-to-registry@v2
id: push id: push
with: with:
image: ${{ steps.build_image.outputs.image }} image: ${{ steps.build_image.outputs.image }}
@ -90,75 +197,41 @@ jobs:
echo "${{ toJSON(steps.push.outputs) }}" echo "${{ toJSON(steps.push.outputs) }}"
build-container-native: build-container-native:
needs: release needs: ci_wf
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read
packages: write packages: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8 uses: actions/download-artifact@v3
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file - name: Move archive file
run: | run: |
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
mkdir -p build/native/nativeCompile/ mkdir -p build/native/nativeCompile/
mv signal-cli build/native/nativeCompile/ chmod +x ./signal-cli-native/signal-cli
chmod +x build/native/nativeCompile/signal-cli mv ./signal-cli-native/signal-cli build/native/nativeCompile/
- name: Build Image - name: Build Image
id: build_image id: build_image
uses: redhat-actions/buildah-build@v3 uses: redhat-actions/buildah-build@v2
with: with:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
containerfiles: ./native.Containerfile containerfiles:
./native.Containerfile
oci: true oci: true
- name: Push To GHCR - name: Push To GHCR
uses: redhat-actions/push-to-registry@v3 uses: redhat-actions/push-to-registry@v2
id: push
with:
image: ${{ steps.build_image.outputs.image }}
tags: ${{ steps.build_image.outputs.tags }}
registry: ${{ env.IMAGE_REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ env.REGISTRY_PASSWORD }}
- name: Echo outputs
run: |
echo "${{ toJSON(steps.push.outputs) }}"
build-container-client:
needs: release
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v6
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8
- name: Move archive file
run: |
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
mkdir -p client/target/release/
mv signal-cli-client client/target/release/
chmod +x client/target/release/signal-cli-client
- name: Build Image
id: build_image
uses: redhat-actions/buildah-build@v3
with:
image: ${{ env.IMAGE_NAME }}
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
containerfiles: ./client.Containerfile
oci: true
- name: Push To GHCR
uses: redhat-actions/push-to-registry@v3
id: push id: push
with: with:
image: ${{ steps.build_image.outputs.image }} image: ${{ steps.build_image.outputs.image }}

8
.gitignore vendored
View File

@ -1,5 +1,4 @@
.gradle/ .gradle/
.kotlin/
.idea/* .idea/*
!.idea/codeStyles/ !.idea/codeStyles/
build/ build/
@ -14,10 +13,3 @@ out/
.DS_Store .DS_Store
/bin/ /bin/
/test-config/ /test-config/
/dist/
/github/
man/*.1
man/*.5
man/man1
man/man5
.superpowers/

View File

@ -5,8 +5,8 @@
<option name="GENERATE_FINAL_LOCALS" value="true" /> <option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" /> <option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" /> <option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" /> <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" /> <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="IMPORT_LAYOUT_TABLE"> <option name="IMPORT_LAYOUT_TABLE">
<value> <value>
<package name="com" withSubpackages="true" static="false" /> <package name="com" withSubpackages="true" static="false" />

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ If you have a question you can ask it in the [GitHub discussions page](https://g
- Be sure to include a **title and clear description**, as much relevant information as possible. - Be sure to include a **title and clear description**, as much relevant information as possible.
- Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using - Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using
- Specify if it's the normal java or the graalvm native version. - Specify if it's the normal java or the graalvm native version.
- Run the failing command with `-vv --scrub-log` flags to get a more detailed log output and include that in the bug report - Run the failing command with `--verbose` flag to get a more detailed log output and include that in the bug report
# Pull request # Pull request

View File

@ -1,4 +1,4 @@
FROM docker.io/azul/zulu-openjdk:25-jre-headless FROM docker.io/eclipse-temurin:17-jre
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger." LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."

View File

@ -3,7 +3,7 @@
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/). signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
It supports registering, verifying, sending and receiving messages. It supports registering, verifying, sending and receiving messages.
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java), signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/lib/libsignal-service). extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal/service).
For registering you need a phone number where you can receive SMS or incoming calls. For registering you need a phone number where you can receive SMS or incoming calls.
signal-cli is primarily intended to be used on servers to notify admins of important events. signal-cli is primarily intended to be used on servers to notify admins of important events.
@ -11,10 +11,6 @@ For this use-case, it has a daemon mode with JSON-RPC interface ([man page](http
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) . and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) .
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust. For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
The official Signal clients expire after three months and then the Signal-Server can make incompatible changes.
So signal-cli releases older than three months may not work correctly.
## Installation ## Installation
You can [build signal-cli](#building) yourself or use You can [build signal-cli](#building) yourself or use
@ -23,33 +19,24 @@ Windows. There's also a [docker image and some Linux packages](https://github.co
System requirements: System requirements:
- at least Java Runtime Environment (JRE) 25 - at least Java Runtime Environment (JRE) 17
- native library: libsignal-client - native library: libsignal-client
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
systems/architectures systems/architectures
see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal) see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
### Install system-wide on Linux [ JVM build ] ### Install system-wide on Linux
See [latest version](https://github.com/AsamK/signal-cli/releases). See [latest version](https://github.com/AsamK/signal-cli/releases).
```sh ```sh
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//') export VERSION=<latest version, format "x.y.z">
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux.tar.gz
sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt sudo tar xf signal-cli-"${VERSION}"-Linux.tar.gz -C /opt
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
``` ```
### Install system-wide on Linux [ GraalVM native build ]
```sh
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux-native.tar.gz
sudo tar xf signal-cli-"${VERSION}"-Linux-native.tar.gz -C /opt
sudo ln -sf /opt/signal-cli /usr/local/bin/
```
You can find further instructions on the Wiki: You can find further instructions on the Wiki:
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart) - [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
@ -64,31 +51,12 @@ Important: The ACCOUNT is your phone number in international format and must inc
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
of all country codes.) of all country codes.)
* Link to an existing account
If you have an existing Signal account associated
with a number, you can link signal-cli to it with:
signal-cli link
* Register a number (with SMS verification) * Register a number (with SMS verification)
Alternatively, if you don't have an existing Signal
account, you can register one from signal-cli. Note
that this will unregister any existing client
associated with the same number.
signal-cli -a ACCOUNT register signal-cli -a ACCOUNT register
You can register Signal using a landline number. In this case, you need to follow the procedure below: You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly
* Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`) to the voice call verification by adding the `--voice` switch at the end of above register command.
* You will get an error `400 (InvalidTransportModeException)`, this is normal
* Wait 60 seconds
* Attempt a voice call verification by adding the `--voice` switch and wait for the call:
```sh
signal-cli -a ACCOUNT register --voice
```
Registering may require solving a CAPTCHA Registering may require solving a CAPTCHA
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha) challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
@ -104,12 +72,6 @@ of all country codes.)
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
``` ```
* Send a message to a username, usernames need to be prefixed with `u:`
```sh
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
```
* Pipe the message content from another process. * Pipe the message content from another process.
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
@ -160,23 +122,14 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
./gradlew run --args="--help" ./gradlew run --args="--help"
``` ```
### JSON Schemas for the JSON-RPC mode
1. Generate [JSON Schema](https://json-schema.org/) files for all the JSON-RPC data classes (`src/main/java/org/asamk/signal/json`):
```sh
./gradlew jsonSchemas
```
2. The generated files can be found in the `build/generated/META-INF/schemas` folder.
### Building a native binary with GraalVM (EXPERIMENTAL) ### Building a native binary with GraalVM (EXPERIMENTAL)
It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not
work in all situations. work in all situations.
1. [Install GraalVM and setup the environment](https://www.graalvm.org/docs/getting-started/#install-graalvm) 1. [Install GraalVM and setup the enviroment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
2. Execute Gradle: 2. [Install prerequisites](https://www.graalvm.org/reference-manual/native-image/#prerequisites)
3. Execute Gradle:
./gradlew nativeCompile ./gradlew nativeCompile

View File

@ -1,46 +1,31 @@
import groovy.json.JsonOutput
plugins { plugins {
java java
application application
eclipse eclipse
`check-lib-versions` `check-lib-versions`
id("org.graalvm.buildtools.native") version "1.1.9" id("org.graalvm.buildtools.native") version "0.9.27"
} }
allprojects { version = "0.12.2"
group = "org.asamk"
version = "0.14.8"
}
java { java {
sourceCompatibility = JavaVersion.VERSION_25 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_25 targetCompatibility = JavaVersion.VERSION_17
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
toolchain {
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
}
}
} }
application { application {
mainClass.set("org.asamk.signal.Main") mainClass.set("org.asamk.signal.Main")
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
} }
graalvmNative { graalvmNative {
binaries { binaries {
this["main"].run { this["main"].run {
buildArgs.add("-Dfile.encoding=UTF-8")
buildArgs.add("-J-Dfile.encoding=UTF-8")
buildArgs.add("-march=compatibility")
buildArgs.add("--enable-native-access=ALL-UNNAMED")
resources.autodetect() resources.autodetect()
configurationFileDirectories.from(file("graalvm-config-dir"))
if (System.getenv("GRAALVM_HOME") == null) { if (System.getenv("GRAALVM_HOME") == null) {
toolchainDetection.set(true) toolchainDetection.set(true)
javaLauncher.set(javaToolchains.launcherFor { javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(25)) languageVersion.set(JavaLanguageVersion.of(17))
}) })
} else { } else {
toolchainDetection.set(false) toolchainDetection.set(false)
@ -49,48 +34,7 @@ graalvmNative {
} }
} }
val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
dependencies { dependencies {
attributesSchema {
attribute(minified)
}
artifactTypes.getByName("jar") {
attributes.attribute(minified, false)
}
}
configurations.runtimeClasspath.configure {
attributes {
attribute(minified, true)
}
}
val excludePatterns = mapOf(
"libsignal-client" to setOf(
"libsignal_jni_testing_amd64.so",
"signal_jni_testing_amd64.dll",
"libsignal_jni_testing_amd64.dylib",
"libsignal_jni_testing_aarch64.dylib",
)
)
val schemaAnnotationProcessor = configurations.create("schemaAnnotationProcessor") {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
registerTransform(JarFileExcluder::class) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
parameters {
excludeFilesByArtifact = excludePatterns
}
}
schemaAnnotationProcessor(libs.micronaut.json.schema.processor)
schemaAnnotationProcessor(libs.micronaut.inject.java)
implementation(libs.bouncycastle) implementation(libs.bouncycastle)
implementation(libs.jackson.databind) implementation(libs.jackson.databind)
implementation(libs.argparse4j) implementation(libs.argparse4j)
@ -98,20 +42,7 @@ dependencies {
implementation(libs.slf4j.api) implementation(libs.slf4j.api)
implementation(libs.slf4j.jul) implementation(libs.slf4j.jul)
implementation(libs.logback) implementation(libs.logback)
implementation(libs.zxing) implementation(project(":lib"))
implementation(libs.micronaut.json.schema.annotations)
if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) {
implementation(libs.micronaut.json.schema.generator)
}
implementation(project(":libsignal-cli"))
testImplementation(libs.junit.jupiter)
testImplementation(platform(libs.junit.jupiter.bom))
testRuntimeOnly(libs.junit.launcher)
}
tasks.named<Test>("test") {
useJUnitPlatform()
} }
configurations { configurations {
@ -135,68 +66,23 @@ tasks.withType<Jar> {
attributes( attributes(
"Implementation-Title" to project.name, "Implementation-Title" to project.name,
"Implementation-Version" to project.version, "Implementation-Version" to project.version,
"Main-Class" to application.mainClass.get(), "Main-Class" to application.mainClass.get()
"Enable-Native-Access" to "ALL-UNNAMED",
) )
} }
} }
tasks.register("fatJar", type = Jar::class) { task("fatJar", type = Jar::class) {
archiveBaseName.set("${project.name}-fat") archiveBaseName.set("${project.name}-fat")
exclude( exclude(
"META-INF/*.SF", "META-INF/*.SF",
"META-INF/**/*.MF",
"META-INF/*.DSA", "META-INF/*.DSA",
"META-INF/*.RSA", "META-INF/*.RSA",
"META-INF/NOTICE*", "META-INF/NOTICE*",
"META-INF/LICENSE*", "META-INF/LICENSE*",
"META-INF/INDEX.LIST", "META-INF/INDEX.LIST",
"**/module-info.class", "**/module-info.class"
) )
duplicatesStrategy = DuplicatesStrategy.WARN duplicatesStrategy = DuplicatesStrategy.WARN
doFirst {
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}
with(tasks.jar.get()) with(tasks.jar.get())
} }
tasks.register("writeLibsignalVersion") {
doLast {
val resolutionResult = configurations.runtimeClasspath.get().incoming.resolutionResult
val libsignalDep =
resolutionResult.allDependencies.find { dep -> dep.requested is ModuleComponentSelector && (dep.requested as ModuleComponentSelector).group == "org.signal" && (dep.requested as ModuleComponentSelector).moduleIdentifier.name == "libsignal-client" }
if (libsignalDep != null) {
val version = (libsignalDep.requested as ModuleComponentSelector).version
file("libsignal-version").writeText(version + "\n")
} else {
throw GradleException("Could not find libsignal-client dependency")
}
}
}
tasks.register<JavaCompile>("jsonSchemas") {
dependsOn(tasks.compileJava)
val schemaBaseUri = "http://localhost:8080/schemas/"
source = sourceSets.main.get().java
include("org/asamk/signal/json/**/*.java")
classpath = sourceSets.main.get().compileClasspath + files(sourceSets.main.get().java.destinationDirectory)
destinationDirectory.set(layout.buildDirectory.dir("generated"))
options.annotationProcessorPath = schemaAnnotationProcessor
options.compilerArgs.addAll(
listOf(
"-Amicronaut.processing.group=org.asamk",
"-Amicronaut.processing.module=signal-cli",
"-Amicronaut.processing.annotations=org.asamk.signal.json.*",
"-Amicronaut.jsonschema.baseUri=$schemaBaseUri",
)
)
doLast {
fileTree(destinationDirectory.get().dir("META-INF/schemas").asFile) {
include("*.schema.json")
}.forEach { schemaFile ->
val normalized = schemaFile.readText().replace("\"$schemaBaseUri/", "\"")
val prettyJson = JsonOutput.prettyPrint(normalized)
schemaFile.writeText("$prettyJson\n")
}
}
}

View File

@ -7,11 +7,11 @@ plugins {
} }
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure { tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
compilerOptions.jvmTarget.set(JvmTarget.JVM_25) compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
} }
java { java {
targetCompatibility = JavaVersion.VERSION_25 targetCompatibility = JavaVersion.VERSION_17
} }
repositories { repositories {

View File

@ -1,12 +1,16 @@
@file:Suppress("DEPRECATION")
import groovy.util.XmlSlurper
import groovy.util.slurpersupport.GPathResult
import org.codehaus.groovy.runtime.ResourceGroovyMethods
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.Dependency
import javax.xml.parsers.DocumentBuilderFactory
class CheckLibVersionsPlugin : Plugin<Project> { class CheckLibVersionsPlugin : Plugin<Project> {
override fun apply(project: Project) { override fun apply(project: Project) {
project.tasks.register("checkLibVersions") { project.task("checkLibVersions") {
description = description =
"Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded." "Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded."
doLast { doLast {
@ -24,15 +28,15 @@ class CheckLibVersionsPlugin : Plugin<Project> {
val name = dependency.name val name = dependency.name
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
try { try {
val dbf = DocumentBuilderFactory.newInstance() val url = ResourceGroovyMethods.toURL(metaDataUrl)
val db = dbf.newDocumentBuilder() val metaDataText = ResourceGroovyMethods.getText(url)
val doc = db.parse(metaDataUrl) val metadata = XmlSlurper().parseText(metaDataText)
val newest = doc.getElementsByTagName("latest").item(0).textContent val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest")
if (version != newest.toString()) { if (version != newest.toString()) {
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}") println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logger.debug("Unable to download or parse {}: {}", metaDataUrl, e.message) logger.debug("Unable to download or parse $metaDataUrl: $e.message")
} }
} }
} }

View File

@ -1,53 +0,0 @@
import org.gradle.api.artifacts.transform.*
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@CacheableTransform
abstract class JarFileExcluder : TransformAction<JarFileExcluder.Parameters> {
interface Parameters : TransformParameters {
@get:Input
var excludeFilesByArtifact: Map<String, Set<String>>
}
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>
override
fun transform(outputs: TransformOutputs) {
val fileName = inputArtifact.get().asFile.name
for (entry in parameters.excludeFilesByArtifact) {
if (fileName.startsWith(entry.key)) {
val nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf("."))
excludeFiles(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}.jar"))
return
}
}
outputs.file(inputArtifact)
}
private fun excludeFiles(artifact: File, excludeFiles: Set<String>, jarFile: File) {
ZipInputStream(FileInputStream(artifact)).use { input ->
ZipOutputStream(FileOutputStream(jarFile)).use { output ->
var entry = input.nextEntry
while (entry != null) {
if (!excludeFiles.contains(entry.name)) {
output.putNextEntry(entry)
input.copyTo(output)
output.closeEntry()
}
entry = input.nextEntry
}
}
}
}
}

View File

@ -1,11 +0,0 @@
FROM docker.io/debian:testing-slim
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
LABEL org.opencontainers.image.licenses=GPL-3.0-only
RUN useradd signal-cli --system
ADD client/target/release/signal-cli-client /usr/bin/signal-cli-client
USER signal-cli
ENTRYPOINT ["/usr/bin/signal-cli-client"]

1448
client/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,18 @@
[package] [package]
name = "signal-cli-client" name = "signal-cli-client"
version = "0.0.1" version = "0.0.1"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] } clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
log = "0.4"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] } tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
jsonrpsee = { version = "0.26", features = [ jsonrpsee = { version = "0.20.0", features = [
"macros", "macros",
"async-client", "async-client",
"http-client", "http-client",
@ -19,4 +20,4 @@ jsonrpsee = { version = "0.26", features = [
bytes = "1" bytes = "1"
tokio-util = "0.7" tokio-util = "0.7"
futures-util = "0.3" futures-util = "0.3"
thiserror = "2" thiserror = "1"

View File

@ -1,6 +1,5 @@
use std::{ffi::OsString, net::SocketAddr};
use clap::{crate_version, Parser, Subcommand, ValueEnum}; use clap::{crate_version, Parser, Subcommand, ValueEnum};
use std::{ffi::OsString, net::SocketAddr};
/// JSON-RPC client for signal-cli /// JSON-RPC client for signal-cli
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@ -10,15 +9,11 @@ pub struct Cli {
#[arg(short = 'a', long)] #[arg(short = 'a', long)]
pub account: Option<String>, pub account: Option<String>,
#[arg(long)]
pub output: Option<String>,
/// TCP host and port of signal-cli daemon /// TCP host and port of signal-cli daemon
#[arg(long, conflicts_with = "json_rpc_http")] #[arg(long, conflicts_with = "json_rpc_http")]
pub json_rpc_tcp: Option<Option<SocketAddr>>, pub json_rpc_tcp: Option<Option<SocketAddr>>,
/// UNIX socket address and port of signal-cli daemon /// UNIX socket address and port of signal-cli daemon
#[cfg(unix)]
#[arg(long, conflicts_with = "json_rpc_tcp")] #[arg(long, conflicts_with = "json_rpc_tcp")]
pub json_rpc_socket: Option<Option<OsString>>, pub json_rpc_socket: Option<Option<OsString>>,
@ -26,6 +21,9 @@ pub struct Cli {
#[arg(long, conflicts_with = "json_rpc_socket")] #[arg(long, conflicts_with = "json_rpc_socket")]
pub json_rpc_http: Option<Option<String>>, pub json_rpc_http: Option<Option<String>>,
#[arg(value_enum, long, default_value_t = OutputTypes::Json)]
pub output: OutputTypes,
#[arg(long)] #[arg(long)]
pub verbose: bool, pub verbose: bool,
@ -33,6 +31,13 @@ pub struct Cli {
pub command: CliCommands, pub command: CliCommands,
} }
#[derive(ValueEnum, Clone, Debug)]
#[value(rename_all = "kebab-case")]
pub enum OutputTypes {
PlainText,
Json,
}
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
#[command(rename_all = "camelCase", version=crate_version!())] #[command(rename_all = "camelCase", version=crate_version!())]
@ -41,10 +46,6 @@ pub enum CliCommands {
#[arg(long)] #[arg(long)]
uri: String, uri: String,
}, },
AddStickerPack {
#[arg(long)]
uri: String,
},
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
Block { Block {
recipient: Vec<String>, recipient: Vec<String>,
@ -56,40 +57,8 @@ pub enum CliCommands {
#[arg(long = "ignore-registered")] #[arg(long = "ignore-registered")]
ignore_registered: Option<bool>, ignore_registered: Option<bool>,
}, },
FinishChangeNumber {
number: String,
#[arg(short = 'v', long = "verification-code")]
verification_code: String,
#[arg(short = 'p', long)]
pin: Option<String>,
},
GetAttachment {
#[arg(long)]
id: String,
#[arg(long)]
recipient: Option<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Option<String>,
},
GetAvatar {
#[arg(long)]
contact: Option<String>,
#[arg(long)]
profile: Option<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Option<String>,
},
GetSticker {
#[arg(long = "pack-id")]
pack_id: String,
#[arg(long = "sticker-id")]
sticker_id: u32,
},
GetUserStatus { GetUserStatus {
recipient: Vec<String>, recipient: Vec<String>,
#[arg(long)]
username: Vec<String>,
}, },
JoinGroup { JoinGroup {
#[arg(long)] #[arg(long)]
@ -97,7 +66,7 @@ pub enum CliCommands {
}, },
Link { Link {
#[arg(short = 'n', long)] #[arg(short = 'n', long)]
name: Option<String>, name: String,
}, },
ListAccounts, ListAccounts,
ListContacts { ListContacts {
@ -108,10 +77,6 @@ pub enum CliCommands {
blocked: Option<bool>, blocked: Option<bool>,
#[arg(long)] #[arg(long)]
name: Option<String>, name: Option<String>,
#[arg(long)]
detailed: bool,
#[arg(long)]
internal: bool,
}, },
ListDevices, ListDevices,
ListGroups { ListGroups {
@ -142,15 +107,11 @@ pub enum CliCommands {
voice: bool, voice: bool,
#[arg(long)] #[arg(long)]
captcha: Option<String>, captcha: Option<String>,
#[arg(long)]
reregister: bool,
}, },
RemoveContact { RemoveContact {
recipient: String, recipient: String,
#[arg(long)] #[arg(long)]
forget: bool, forget: bool,
#[arg(long)]
hide: bool,
}, },
RemoveDevice { RemoveDevice {
#[arg(short = 'd', long = "device-id")] #[arg(short = 'd', long = "device-id")]
@ -176,36 +137,21 @@ pub enum CliCommands {
#[arg(short = 'g', long)] #[arg(short = 'g', long)]
group_id: Vec<String>, group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(long)] #[arg(long)]
note_to_self: bool, note_to_self: bool,
#[arg(long)]
notify_self: bool,
#[arg(short = 'e', long)] #[arg(short = 'e', long)]
end_session: bool, end_session: bool,
#[arg(short = 'm', long)] #[arg(short = 'm', long)]
message: Option<String>, message: Option<String>,
#[arg(long)]
message_from_stdin: bool,
#[arg(short = 'a', long)] #[arg(short = 'a', long)]
attachment: Vec<String>, attachment: Vec<String>,
#[arg(long)]
view_once: bool,
#[arg(long)] #[arg(long)]
mention: Vec<String>, mention: Vec<String>,
#[arg(long)]
text_style: Vec<String>,
#[arg(long)] #[arg(long)]
quote_timestamp: Option<u64>, quote_timestamp: Option<u64>,
@ -218,24 +164,6 @@ pub enum CliCommands {
#[arg(long)] #[arg(long)]
quote_mention: Vec<String>, quote_mention: Vec<String>,
#[arg(long)]
quote_text_style: Vec<String>,
#[arg(long)]
quote_attachment: Vec<String>,
#[arg(long)]
preview_url: Option<String>,
#[arg(long)]
preview_title: Option<String>,
#[arg(long)]
preview_description: Option<String>,
#[arg(long)]
preview_image: Option<String>,
#[arg(long)] #[arg(long)]
sticker: Option<String>, sticker: Option<String>,
@ -244,28 +172,6 @@ pub enum CliCommands {
#[arg(long)] #[arg(long)]
story_author: Option<String>, story_author: Option<String>,
#[arg(long)]
edit_timestamp: Option<u64>,
#[arg(long = "no-urgent")]
no_urgent: bool,
},
SendAdminDelete {
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'a', long = "target-author")]
target_author: String,
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: u64,
#[arg(long)]
story: bool,
#[arg(long)]
notify_self: bool,
}, },
SendContacts, SendContacts,
SendPaymentNotification { SendPaymentNotification {
@ -277,117 +183,15 @@ pub enum CliCommands {
#[arg(long)] #[arg(long)]
note: String, note: String,
}, },
SendPinMessage {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(short = 'a', long = "target-author")]
target_author: String,
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: u64,
#[arg(short = 'd', long = "pin-duration")]
pin_duration: Option<i32>,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
#[arg(long)]
story: bool,
},
SendPollCreate {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(short = 'q', long = "question")]
question: String,
#[arg(short = 'o', long = "option")]
option: Vec<String>,
#[arg(long = "no-multi")]
no_multi: bool,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
},
SendPollTerminate {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(long = "poll-timestamp")]
poll_timestamp: u64,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
},
SendPollVote {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(long = "poll-author")]
poll_author: Option<String>,
#[arg(long = "poll-timestamp")]
poll_timestamp: u64,
#[arg(short = 'o', long = "option")]
option: Vec<i32>,
#[arg(long = "vote-count")]
vote_count: i32,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
},
SendReaction { SendReaction {
recipient: Vec<String>, recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")] #[arg(short = 'g', long = "group-id")]
group_id: Vec<String>, group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(long = "note-to-self")] #[arg(long = "note-to-self")]
note_to_self: bool, note_to_self: bool,
#[arg(long)]
notify_self: bool,
#[arg(short = 'e', long)] #[arg(short = 'e', long)]
emoji: String, emoji: String,
@ -406,9 +210,6 @@ pub enum CliCommands {
SendReceipt { SendReceipt {
recipient: String, recipient: String,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(short = 't', long = "target-timestamp")] #[arg(short = 't', long = "target-timestamp")]
target_timestamp: Vec<u64>, target_timestamp: Vec<u64>,
@ -425,49 +226,9 @@ pub enum CliCommands {
#[arg(short = 's', long)] #[arg(short = 's', long)]
stop: bool, stop: bool,
}, },
SendUnpinMessage {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(short = 'a', long = "target-author")]
target_author: String,
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: u64,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
#[arg(long)]
story: bool,
},
SendMessageRequestResponse {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(long)]
r#type: MessageRequestResponseType,
},
SetPin { SetPin {
pin: String, pin: String,
}, },
StartChangeNumber {
number: String,
#[arg(short = 'v', long)]
voice: bool,
#[arg(long)]
captcha: Option<String>,
},
SubmitRateLimitChallenge { SubmitRateLimitChallenge {
challenge: String, challenge: String,
captcha: String, captcha: String,
@ -495,16 +256,6 @@ pub enum CliCommands {
UpdateAccount { UpdateAccount {
#[arg(short = 'n', long = "device-name")] #[arg(short = 'n', long = "device-name")]
device_name: Option<String>, device_name: Option<String>,
#[arg(long = "unrestricted-unidentified-sender")]
unrestricted_unidentified_sender: Option<bool>,
#[arg(long = "discoverable-by-number")]
discoverable_by_number: Option<bool>,
#[arg(long = "number-sharing")]
number_sharing: Option<bool>,
#[arg(short = 'u', long = "username")]
username: Option<String>,
#[arg(long = "delete-username")]
delete_username: bool,
}, },
UpdateConfiguration { UpdateConfiguration {
#[arg(long = "read-receipts")] #[arg(long = "read-receipts")]
@ -527,28 +278,6 @@ pub enum CliCommands {
#[arg(short = 'n', long)] #[arg(short = 'n', long)]
name: Option<String>, name: Option<String>,
#[arg(long = "given-name")]
given_name: Option<String>,
#[arg(long = "family-name")]
family_name: Option<String>,
#[arg(long = "nick-given-name")]
nick_given_name: Option<String>,
#[arg(long = "nick-family-name")]
nick_family_name: Option<String>,
#[arg(long)]
note: Option<String>,
},
UpdateDevice {
#[arg(short = 'd', long = "device-id")]
device_id: u32,
#[arg(short = 'n', long = "device-name")]
device_name: String,
}, },
UpdateGroup { UpdateGroup {
#[arg(short = 'g', long = "group-id")] #[arg(short = 'g', long = "group-id")]
@ -598,12 +327,6 @@ pub enum CliCommands {
#[arg(short = 'e', long)] #[arg(short = 'e', long)]
expiration: Option<u32>, expiration: Option<u32>,
#[arg(long = "member-label-emoji")]
member_label_emoji: Option<String>,
#[arg(long = "member-label")]
member_label: Option<String>,
}, },
UpdateProfile { UpdateProfile {
#[arg(long = "given-name")] #[arg(long = "given-name")]
@ -618,7 +341,7 @@ pub enum CliCommands {
#[arg(long = "about-emoji")] #[arg(long = "about-emoji")]
about_emoji: Option<String>, about_emoji: Option<String>,
#[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")] #[arg(long = "mobile-coin-address")]
mobile_coin_address: Option<String>, mobile_coin_address: Option<String>,
#[arg(long)] #[arg(long)]
@ -660,10 +383,3 @@ pub enum GroupPermission {
EveryMember, EveryMember,
OnlyAdmins, OnlyAdmins,
} }
#[derive(ValueEnum, Clone, Debug)]
#[value(rename_all = "kebab-case")]
pub enum MessageRequestResponseType {
Accept,
Delete,
}

View File

@ -1,7 +1,8 @@
use std::path::Path; use std::path::Path;
use jsonrpsee::async_client::ClientBuilder; use jsonrpsee::async_client::ClientBuilder;
use jsonrpsee::core::client::{Error, SubscriptionClientT}; use jsonrpsee::core::client::SubscriptionClientT;
use jsonrpsee::core::Error;
use jsonrpsee::http_client::HttpClientBuilder; use jsonrpsee::http_client::HttpClientBuilder;
use jsonrpsee::proc_macros::rpc; use jsonrpsee::proc_macros::rpc;
use serde::Deserialize; use serde::Deserialize;
@ -17,13 +18,6 @@ pub trait Rpc {
uri: String, uri: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "addStickerPack", param_kind = map)]
async fn add_sticker_pack(
&self,
account: Option<String>,
uri: String,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "block", param_kind = map)] #[method(name = "block", param_kind = map)]
fn block( fn block(
&self, &self,
@ -39,58 +33,21 @@ pub trait Rpc {
#[allow(non_snake_case)] ignoreRegistered: Option<bool>, #[allow(non_snake_case)] ignoreRegistered: Option<bool>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "getAttachment", param_kind = map)]
fn get_attachment(
&self,
account: Option<String>,
id: String,
recipient: Option<String>,
#[allow(non_snake_case)] groupId: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "getAvatar", param_kind = map)]
fn get_avatar(
&self,
account: Option<String>,
contact: Option<String>,
profile: Option<String>,
#[allow(non_snake_case)] groupId: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "getSticker", param_kind = map)]
fn get_sticker(
&self,
account: Option<String>,
#[allow(non_snake_case)] packId: String,
#[allow(non_snake_case)] stickerId: u32,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "getUserStatus", param_kind = map)] #[method(name = "getUserStatus", param_kind = map)]
fn get_user_status( fn get_user_status(
&self, &self,
account: Option<String>, account: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
usernames: Vec<String>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "joinGroup", param_kind = map)] #[method(name = "joinGroup", param_kind = map)]
fn join_group(&self, account: Option<String>, uri: String) -> Result<Value, ErrorObjectOwned>; fn join_group(&self, account: Option<String>, uri: String) -> Result<Value, ErrorObjectOwned>;
#[allow(non_snake_case)]
#[method(name = "finishChangeNumber", param_kind = map)]
fn finish_change_number(
&self,
account: Option<String>,
number: String,
verificationCode: String,
pin: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "finishLink", param_kind = map)] #[method(name = "finishLink", param_kind = map)]
fn finish_link( fn finish_link(
&self, &self,
#[allow(non_snake_case)] deviceLinkUri: String, #[allow(non_snake_case)] deviceLinkUri: String,
#[allow(non_snake_case)] deviceName: Option<String>, #[allow(non_snake_case)] deviceName: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listAccounts", param_kind = map)] #[method(name = "listAccounts", param_kind = map)]
@ -104,8 +61,6 @@ pub trait Rpc {
#[allow(non_snake_case)] allRecipients: bool, #[allow(non_snake_case)] allRecipients: bool,
blocked: Option<bool>, blocked: Option<bool>,
name: Option<String>, name: Option<String>,
detailed: bool,
internal: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listDevices", param_kind = map)] #[method(name = "listDevices", param_kind = map)]
@ -143,7 +98,6 @@ pub trait Rpc {
account: Option<String>, account: Option<String>,
voice: bool, voice: bool,
captcha: Option<String>, captcha: Option<String>,
reregister: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "removeContact", param_kind = map)] #[method(name = "removeContact", param_kind = map)]
@ -152,7 +106,6 @@ pub trait Rpc {
account: Option<String>, account: Option<String>,
recipient: String, recipient: String,
forget: bool, forget: bool,
hide: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "removeDevice", param_kind = map)] #[method(name = "removeDevice", param_kind = map)]
@ -175,123 +128,29 @@ pub trait Rpc {
#[allow(non_snake_case)] noteToSelf: bool, #[allow(non_snake_case)] noteToSelf: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[allow(non_snake_case)]
#[method(name = "send", param_kind = map)] #[method(name = "send", param_kind = map)]
fn send( fn send(
&self, &self,
account: Option<String>, account: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
groupIds: Vec<String>, #[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] notifySelf: bool,
#[allow(non_snake_case)] noteToSelf: bool, #[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] endSession: bool, #[allow(non_snake_case)] endSession: bool,
message: String, message: String,
attachments: Vec<String>, attachments: Vec<String>,
#[allow(non_snake_case)] viewOnce: bool,
mentions: Vec<String>, mentions: Vec<String>,
#[allow(non_snake_case)] textStyle: Vec<String>,
#[allow(non_snake_case)] quoteTimestamp: Option<u64>, #[allow(non_snake_case)] quoteTimestamp: Option<u64>,
#[allow(non_snake_case)] quoteAuthor: Option<String>, #[allow(non_snake_case)] quoteAuthor: Option<String>,
#[allow(non_snake_case)] quoteMessage: Option<String>, #[allow(non_snake_case)] quoteMessage: Option<String>,
#[allow(non_snake_case)] quoteMention: Vec<String>, #[allow(non_snake_case)] quoteMention: Vec<String>,
#[allow(non_snake_case)] quoteTextStyle: Vec<String>,
#[allow(non_snake_case)] quoteAttachment: Vec<String>,
#[allow(non_snake_case)] previewUrl: Option<String>,
#[allow(non_snake_case)] previewTitle: Option<String>,
#[allow(non_snake_case)] previewDescription: Option<String>,
#[allow(non_snake_case)] previewImage: Option<String>,
sticker: Option<String>, sticker: Option<String>,
#[allow(non_snake_case)] storyTimestamp: Option<u64>, #[allow(non_snake_case)] storyTimestamp: Option<u64>,
#[allow(non_snake_case)] storyAuthor: Option<String>, #[allow(non_snake_case)] storyAuthor: Option<String>,
#[allow(non_snake_case)] editTimestamp: Option<u64>,
#[allow(non_snake_case)] noUrgent: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendContacts", param_kind = map)] #[method(name = "sendContacts", param_kind = map)]
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>; fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendAdminDelete", param_kind = map)]
fn send_admin_delete(
&self,
account: Option<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
#[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64,
story: bool,
#[allow(non_snake_case)] notifySelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPinMessage", param_kind = map)]
fn send_pin_message(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64,
#[allow(non_snake_case)] pinDuration: Option<i32>,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
story: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPollCreate", param_kind = map)]
fn send_poll_create(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
question: String,
option: Vec<String>,
#[allow(non_snake_case)] noMulti: bool,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPollVote", param_kind = map)]
fn send_poll_vote(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] pollAuthor: Option<String>,
#[allow(non_snake_case)] pollTimestamp: u64,
option: Vec<i32>,
#[allow(non_snake_case)] voteCount: i32,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPollTerminate", param_kind = map)]
fn send_poll_terminate(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] pollTimestamp: u64,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendUnpinMessage", param_kind = map)]
fn send_unpin_message(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
story: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPaymentNotification", param_kind = map)] #[method(name = "sendPaymentNotification", param_kind = map)]
fn send_payment_notification( fn send_payment_notification(
&self, &self,
@ -307,9 +166,7 @@ pub trait Rpc {
account: Option<String>, account: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>, #[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] noteToSelf: bool, #[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
emoji: String, emoji: String,
#[allow(non_snake_case)] targetAuthor: String, #[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64, #[allow(non_snake_case)] targetTimestamp: u64,
@ -322,7 +179,6 @@ pub trait Rpc {
&self, &self,
account: Option<String>, account: Option<String>,
recipient: String, recipient: String,
usernames: Vec<String>,
#[allow(non_snake_case)] targetTimestamps: Vec<u64>, #[allow(non_snake_case)] targetTimestamps: Vec<u64>,
r#type: String, r#type: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
@ -339,15 +195,6 @@ pub trait Rpc {
stop: bool, stop: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendMessageRequestResponse", param_kind = map)]
fn send_message_request_response(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
r#type: String,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "setPin", param_kind = map)] #[method(name = "setPin", param_kind = map)]
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>; fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>;
@ -359,15 +206,6 @@ pub trait Rpc {
captcha: String, captcha: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "startChangeNumber", param_kind = map)]
fn start_change_number(
&self,
account: Option<String>,
number: String,
voice: bool,
captcha: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "startLink", param_kind = map)] #[method(name = "startLink", param_kind = map)]
fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>; fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>;
@ -395,24 +233,18 @@ pub trait Rpc {
#[allow(non_snake_case)] deleteAccount: bool, #[allow(non_snake_case)] deleteAccount: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[allow(non_snake_case)]
#[method(name = "updateAccount", param_kind = map)] #[method(name = "updateAccount", param_kind = map)]
fn update_account( fn update_account(
&self, &self,
account: Option<String>, account: Option<String>,
deviceName: Option<String>, #[allow(non_snake_case)] deviceName: Option<String>,
unrestrictedUnidentifiedSender: Option<bool>,
discoverableByNumber: Option<bool>,
numberSharing: Option<bool>,
username: Option<String>,
deleteUsername: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateConfiguration", param_kind = map)] #[method(name = "updateConfiguration", param_kind = map)]
fn update_configuration( fn update_configuration(
&self, &self,
account: Option<String>, account: Option<String>,
#[allow(non_snake_case)] readReceipts: Option<bool>, #[allow(non_snake_case)] readReceiptes: Option<bool>,
#[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>, #[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>,
#[allow(non_snake_case)] typingIndicators: Option<bool>, #[allow(non_snake_case)] typingIndicators: Option<bool>,
#[allow(non_snake_case)] linkPreviews: Option<bool>, #[allow(non_snake_case)] linkPreviews: Option<bool>,
@ -425,19 +257,6 @@ pub trait Rpc {
recipient: String, recipient: String,
name: Option<String>, name: Option<String>,
expiration: Option<u32>, expiration: Option<u32>,
#[allow(non_snake_case)] givenName: Option<String>,
#[allow(non_snake_case)] familyName: Option<String>,
#[allow(non_snake_case)] nickGivenName: Option<String>,
#[allow(non_snake_case)] nickFamilyName: Option<String>,
note: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateDevice", param_kind = map)]
fn update_device(
&self,
account: Option<String>,
#[allow(non_snake_case)] deviceId: u32,
#[allow(non_snake_case)] deviceName: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateGroup", param_kind = map)] #[method(name = "updateGroup", param_kind = map)]
@ -460,8 +279,6 @@ pub trait Rpc {
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>, #[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>, #[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
expiration: Option<u32>, expiration: Option<u32>,
#[allow(non_snake_case)] memberLabelEmoji: Option<String>,
#[allow(non_snake_case)] memberLabel: Option<String>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateProfile", param_kind = map)] #[method(name = "updateProfile", param_kind = map)]
@ -510,23 +327,20 @@ pub struct JsonLink {
pub device_link_uri: String, pub device_link_uri: String,
} }
pub async fn connect_tcp( pub async fn connect_tcp(tcp: impl ToSocketAddrs) -> Result<impl SubscriptionClientT, Error> {
tcp: impl ToSocketAddrs,
) -> Result<impl SubscriptionClientT, std::io::Error> {
let (sender, receiver) = super::transports::tcp::connect(tcp).await?; let (sender, receiver) = super::transports::tcp::connect(tcp).await?;
Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
} }
#[cfg(unix)]
pub async fn connect_unix( pub async fn connect_unix(
socket_path: impl AsRef<Path>, socket_path: impl AsRef<Path>,
) -> Result<impl SubscriptionClientT, std::io::Error> { ) -> Result<impl SubscriptionClientT, Error> {
let (sender, receiver) = super::transports::ipc::connect(socket_path).await?; let (sender, receiver) = super::transports::ipc::connect(socket_path).await?;
Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
} }
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> { pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT, Error> {
HttpClientBuilder::default().build(uri) HttpClientBuilder::default().build(uri)
} }

View File

@ -1,13 +1,13 @@
use std::{path::PathBuf, time::Duration}; use std::{path::PathBuf, time::Duration};
use clap::Parser; use clap::Parser;
use jsonrpsee::core::client::{Error as RpcError, Subscription, SubscriptionClientT}; use cli::Cli;
use serde_json::{Error, Value}; use jsonrpsee::core::client::{Subscription, SubscriptionClientT};
use jsonrpsee::core::Error as RpcError;
use serde_json::Value;
use tokio::{select, time::sleep}; use tokio::{select, time::sleep};
use cli::Cli; use crate::cli::{GroupPermission, LinkState};
use crate::cli::{CliCommands, GroupPermission, LinkState};
use crate::jsonrpc::RpcClient; use crate::jsonrpc::RpcClient;
mod cli; mod cli;
@ -38,7 +38,7 @@ async fn handle_command(
client: impl SubscriptionClientT + Sync, client: impl SubscriptionClientT + Sync,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {
match cli.command { match cli.command {
CliCommands::Receive { timeout } => { cli::CliCommands::Receive { timeout } => {
let mut stream = client.subscribe_receive(cli.account).await?; let mut stream = client.subscribe_receive(cli.account).await?;
{ {
@ -50,63 +50,50 @@ async fn handle_command(
stream.unsubscribe().await?; stream.unsubscribe().await?;
Ok(Value::Null) Ok(Value::Null)
} }
CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await, cli::CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
CliCommands::Block { cli::CliCommands::Block {
recipient, recipient,
group_id, group_id,
} => client.block(cli.account, recipient, group_id).await, } => client.block(cli.account, recipient, group_id).await,
CliCommands::DeleteLocalAccountData { ignore_registered } => { cli::CliCommands::DeleteLocalAccountData { ignore_registered } => {
client client
.delete_local_account_data(cli.account, ignore_registered) .delete_local_account_data(cli.account, ignore_registered)
.await .await
} }
CliCommands::GetUserStatus { cli::CliCommands::GetUserStatus { recipient } => {
recipient, client.get_user_status(cli.account, recipient).await
username,
} => {
client
.get_user_status(cli.account, recipient, username)
.await
} }
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await, cli::CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
CliCommands::Link { name } => { cli::CliCommands::Link { name } => {
let url = client let url = client
.start_link(cli.account) .start_link(cli.account)
.await .await
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))? .map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
.device_link_uri; .device_link_uri;
println!("{url}"); println!("{}", url);
client.finish_link(url, name).await client.finish_link(url, name).await
} }
CliCommands::ListAccounts => client.list_accounts().await, cli::CliCommands::ListAccounts => client.list_accounts().await,
CliCommands::ListContacts { cli::CliCommands::ListContacts {
recipient, recipient,
all_recipients, all_recipients,
blocked, blocked,
name, name,
detailed,
internal,
} => { } => {
client client
.list_contacts( .list_contacts(cli.account, recipient, all_recipients, blocked, name)
cli.account,
recipient,
all_recipients,
blocked,
name,
detailed,
internal,
)
.await .await
} }
CliCommands::ListDevices => client.list_devices(cli.account).await, cli::CliCommands::ListDevices => client.list_devices(cli.account).await,
CliCommands::ListGroups { cli::CliCommands::ListGroups {
detailed: _, detailed: _,
group_id, group_id,
} => client.list_groups(cli.account, group_id).await, } => client.list_groups(cli.account, group_id).await,
CliCommands::ListIdentities { number } => client.list_identities(cli.account, number).await, cli::CliCommands::ListIdentities { number } => {
CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await, client.list_identities(cli.account, number).await
CliCommands::QuitGroup { }
cli::CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
cli::CliCommands::QuitGroup {
group_id, group_id,
delete, delete,
admin, admin,
@ -115,29 +102,17 @@ async fn handle_command(
.quit_group(cli.account, group_id, delete, admin) .quit_group(cli.account, group_id, delete, admin)
.await .await
} }
CliCommands::Register { cli::CliCommands::Register { voice, captcha } => {
voice, client.register(cli.account, voice, captcha).await
captcha,
reregister,
} => {
client
.register(cli.account, voice, captcha, reregister)
.await
} }
CliCommands::RemoveContact { cli::CliCommands::RemoveContact { recipient, forget } => {
recipient, client.remove_contact(cli.account, recipient, forget).await
forget,
hide,
} => {
client
.remove_contact(cli.account, recipient, forget, hide)
.await
} }
CliCommands::RemoveDevice { device_id } => { cli::CliCommands::RemoveDevice { device_id } => {
client.remove_device(cli.account, device_id).await client.remove_device(cli.account, device_id).await
} }
CliCommands::RemovePin => client.remove_pin(cli.account).await, cli::CliCommands::RemovePin => client.remove_pin(cli.account).await,
CliCommands::RemoteDelete { cli::CliCommands::RemoteDelete {
target_timestamp, target_timestamp,
recipient, recipient,
group_id, group_id,
@ -153,91 +128,44 @@ async fn handle_command(
) )
.await .await
} }
CliCommands::Send { cli::CliCommands::Send {
recipient, recipient,
group_id, group_id,
username,
notify_self,
note_to_self, note_to_self,
end_session, end_session,
message, message,
message_from_stdin,
attachment, attachment,
view_once,
mention, mention,
text_style,
quote_timestamp, quote_timestamp,
quote_author, quote_author,
quote_message, quote_message,
quote_mention, quote_mention,
quote_text_style,
quote_attachment,
preview_url,
preview_title,
preview_description,
preview_image,
sticker, sticker,
story_timestamp, story_timestamp,
story_author, story_author,
edit_timestamp,
no_urgent,
} => { } => {
client client
.send( .send(
cli.account, cli.account,
recipient, recipient,
group_id, group_id,
username,
notify_self,
note_to_self, note_to_self,
end_session, end_session,
if message_from_stdin { message.unwrap_or_default(),
std::io::read_to_string(std::io::stdin()).unwrap()
} else {
message.unwrap_or_default()
},
attachment, attachment,
view_once,
mention, mention,
text_style,
quote_timestamp, quote_timestamp,
quote_author, quote_author,
quote_message, quote_message,
quote_mention, quote_mention,
quote_text_style,
quote_attachment,
preview_url,
preview_title,
preview_description,
preview_image,
sticker, sticker,
story_timestamp, story_timestamp,
story_author, story_author,
edit_timestamp,
no_urgent,
) )
.await .await
} }
CliCommands::SendContacts => client.send_contacts(cli.account).await, cli::CliCommands::SendContacts => client.send_contacts(cli.account).await,
CliCommands::SendAdminDelete { cli::CliCommands::SendPaymentNotification {
group_id,
target_author,
target_timestamp,
story,
notify_self,
} => {
client
.send_admin_delete(
cli.account,
group_id,
target_author,
target_timestamp,
story,
notify_self,
)
.await
}
CliCommands::SendPaymentNotification {
recipient, recipient,
receipt, receipt,
note, note,
@ -246,108 +174,10 @@ async fn handle_command(
.send_payment_notification(cli.account, recipient, receipt, note) .send_payment_notification(cli.account, recipient, receipt, note)
.await .await
} }
CliCommands::SendPinMessage { cli::CliCommands::SendReaction {
recipient, recipient,
group_id, group_id,
username,
target_author,
target_timestamp,
pin_duration,
note_to_self, note_to_self,
notify_self,
story,
} => {
client
.send_pin_message(
cli.account,
recipient,
group_id,
username,
target_author,
target_timestamp,
pin_duration,
note_to_self,
notify_self,
story,
)
.await
}
CliCommands::SendPollCreate {
recipient,
group_id,
username,
question,
option,
no_multi,
note_to_self,
notify_self,
} => {
client
.send_poll_create(
cli.account,
recipient,
group_id,
username,
question,
option,
no_multi,
note_to_self,
notify_self,
)
.await
}
CliCommands::SendPollTerminate {
recipient,
group_id,
username,
poll_timestamp,
note_to_self,
notify_self,
} => {
client
.send_poll_terminate(
cli.account,
recipient,
group_id,
username,
poll_timestamp,
note_to_self,
notify_self,
)
.await
}
CliCommands::SendPollVote {
recipient,
group_id,
username,
poll_author,
poll_timestamp,
option,
vote_count,
note_to_self,
notify_self,
} => {
client
.send_poll_vote(
cli.account,
recipient,
group_id,
username,
poll_author,
poll_timestamp,
option,
vote_count,
note_to_self,
notify_self,
)
.await
}
CliCommands::SendReaction {
recipient,
group_id,
username,
note_to_self,
notify_self,
emoji, emoji,
target_author, target_author,
target_timestamp, target_timestamp,
@ -359,9 +189,7 @@ async fn handle_command(
cli.account, cli.account,
recipient, recipient,
group_id, group_id,
username,
note_to_self, note_to_self,
notify_self,
emoji, emoji,
target_author, target_author,
target_timestamp, target_timestamp,
@ -370,9 +198,8 @@ async fn handle_command(
) )
.await .await
} }
CliCommands::SendReceipt { cli::CliCommands::SendReceipt {
recipient, recipient,
username,
target_timestamp, target_timestamp,
r#type, r#type,
} => { } => {
@ -380,7 +207,6 @@ async fn handle_command(
.send_receipt( .send_receipt(
cli.account, cli.account,
recipient, recipient,
username,
target_timestamp, target_timestamp,
match r#type { match r#type {
cli::ReceiptType::Read => "read".to_owned(), cli::ReceiptType::Read => "read".to_owned(),
@ -389,8 +215,8 @@ async fn handle_command(
) )
.await .await
} }
CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await, cli::CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
CliCommands::SendTyping { cli::CliCommands::SendTyping {
recipient, recipient,
group_id, group_id,
stop, stop,
@ -399,37 +225,13 @@ async fn handle_command(
.send_typing(cli.account, recipient, group_id, stop) .send_typing(cli.account, recipient, group_id, stop)
.await .await
} }
CliCommands::SendUnpinMessage { cli::CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
recipient, cli::CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
group_id,
username,
target_author,
target_timestamp,
note_to_self,
notify_self,
story,
} => {
client
.send_unpin_message(
cli.account,
recipient,
group_id,
username,
target_author,
target_timestamp,
note_to_self,
notify_self,
story,
)
.await
}
CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
client client
.submit_rate_limit_challenge(cli.account, challenge, captcha) .submit_rate_limit_challenge(cli.account, challenge, captcha)
.await .await
} }
CliCommands::Trust { cli::CliCommands::Trust {
recipient, recipient,
trust_all_known_keys, trust_all_known_keys,
verified_safety_number, verified_safety_number,
@ -443,34 +245,17 @@ async fn handle_command(
) )
.await .await
} }
CliCommands::Unblock { cli::CliCommands::Unblock {
recipient, recipient,
group_id, group_id,
} => client.unblock(cli.account, recipient, group_id).await, } => client.unblock(cli.account, recipient, group_id).await,
CliCommands::Unregister { delete_account } => { cli::CliCommands::Unregister { delete_account } => {
client.unregister(cli.account, delete_account).await client.unregister(cli.account, delete_account).await
} }
CliCommands::UpdateAccount { cli::CliCommands::UpdateAccount { device_name } => {
device_name, client.update_account(cli.account, device_name).await
unrestricted_unidentified_sender,
discoverable_by_number,
number_sharing,
username,
delete_username,
} => {
client
.update_account(
cli.account,
device_name,
unrestricted_unidentified_sender,
discoverable_by_number,
number_sharing,
username,
delete_username,
)
.await
} }
CliCommands::UpdateConfiguration { cli::CliCommands::UpdateConfiguration {
read_receipts, read_receipts,
unidentified_delivery_indicators, unidentified_delivery_indicators,
typing_indicators, typing_indicators,
@ -486,39 +271,16 @@ async fn handle_command(
) )
.await .await
} }
CliCommands::UpdateContact { cli::CliCommands::UpdateContact {
recipient, recipient,
expiration, expiration,
name, name,
given_name,
family_name,
nick_given_name,
nick_family_name,
note,
} => { } => {
client client
.update_contact( .update_contact(cli.account, recipient, name, expiration)
cli.account,
recipient,
name,
expiration,
given_name,
family_name,
nick_given_name,
nick_family_name,
note,
)
.await .await
} }
CliCommands::UpdateDevice { cli::CliCommands::UpdateGroup {
device_id,
device_name,
} => {
client
.update_device(cli.account, device_id, device_name)
.await
}
CliCommands::UpdateGroup {
group_id, group_id,
name, name,
description, description,
@ -535,8 +297,6 @@ async fn handle_command(
set_permission_edit_details, set_permission_edit_details,
set_permission_send_messages, set_permission_send_messages,
expiration, expiration,
member_label_emoji,
member_label,
} => { } => {
client client
.update_group( .update_group(
@ -570,12 +330,10 @@ async fn handle_command(
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(), GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
}), }),
expiration, expiration,
member_label_emoji,
member_label,
) )
.await .await
} }
CliCommands::UpdateProfile { cli::CliCommands::UpdateProfile {
given_name, given_name,
family_name, family_name,
about, about,
@ -597,72 +355,14 @@ async fn handle_command(
) )
.await .await
} }
CliCommands::UploadStickerPack { path } => { cli::CliCommands::UploadStickerPack { path } => {
client.upload_sticker_pack(cli.account, path).await client.upload_sticker_pack(cli.account, path).await
} }
CliCommands::Verify { cli::CliCommands::Verify {
verification_code, verification_code,
pin, pin,
} => client.verify(cli.account, verification_code, pin).await, } => client.verify(cli.account, verification_code, pin).await,
CliCommands::Version => client.version().await, cli::CliCommands::Version => client.version().await,
CliCommands::AddStickerPack { uri } => client.add_sticker_pack(cli.account, uri).await,
CliCommands::FinishChangeNumber {
number,
verification_code,
pin,
} => {
client
.finish_change_number(cli.account, number, verification_code, pin)
.await
}
CliCommands::GetAttachment {
id,
recipient,
group_id,
} => {
client
.get_attachment(cli.account, id, recipient, group_id)
.await
}
CliCommands::GetAvatar {
contact,
profile,
group_id,
} => {
client
.get_avatar(cli.account, contact, profile, group_id)
.await
}
CliCommands::GetSticker {
pack_id,
sticker_id,
} => client.get_sticker(cli.account, pack_id, sticker_id).await,
CliCommands::StartChangeNumber {
number,
voice,
captcha,
} => {
client
.start_change_number(cli.account, number, voice, captcha)
.await
}
CliCommands::SendMessageRequestResponse {
recipient,
group_id,
r#type,
} => {
client
.send_message_request_response(
cli.account,
recipient,
group_id,
match r#type {
cli::MessageRequestResponseType::Accept => "accept".to_owned(),
cli::MessageRequestResponseType::Delete => "delete".to_owned(),
},
)
.await
}
} }
} }
@ -686,12 +386,6 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await handle_command(cli, client).await
} else { } else {
#[cfg(windows)]
{
Err(RpcError::Custom("Invalid socket".into()))
}
#[cfg(unix)]
{
let socket_path = cli let socket_path = cli
.json_rpc_socket .json_rpc_socket
.clone() .clone()
@ -711,12 +405,11 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await handle_command(cli, client).await
} }
} }
}
async fn stream_next( async fn stream_next(
timeout: f64, timeout: f64,
stream: &mut Subscription<Value>, stream: &mut Subscription<Value>,
) -> Option<Result<Value, Error>> { ) -> Option<Result<Value, RpcError>> {
if timeout < 0.0 { if timeout < 0.0 {
stream.next().await stream.next().await
} else { } else {

View File

@ -1,8 +1,8 @@
use std::io::Error;
use std::path::Path; use std::path::Path;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT}; use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
use jsonrpsee::core::Error;
use tokio::net::UnixStream; use tokio::net::UnixStream;
use tokio_util::codec::Decoder; use tokio_util::codec::Decoder;

View File

@ -1,8 +1,10 @@
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream}; use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT}; use jsonrpsee::core::{
async_trait,
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
};
use thiserror::Error; use thiserror::Error;
#[cfg(unix)]
pub mod ipc; pub mod ipc;
mod stream_codec; mod stream_codec;
pub mod tcp; pub mod tcp;
@ -19,6 +21,7 @@ struct Sender<T: Send + Sink<String>> {
inner: T, inner: T,
} }
#[async_trait]
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
for Sender<T> for Sender<T>
{ {
@ -28,7 +31,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner self.inner
.send(body) .send(body)
.await .await
.map_err(|e| Errors::Other(format!("{e:?}")))?; .map_err(|e| Errors::Other(format!("{:?}", e)))?;
Ok(()) Ok(())
} }
@ -36,7 +39,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner self.inner
.close() .close()
.await .await
.map_err(|e| Errors::Other(format!("{e:?}")))?; .map_err(|e| Errors::Other(format!("{:?}", e)))?;
Ok(()) Ok(())
} }
} }
@ -45,6 +48,7 @@ struct Receiver<T: Send + Stream> {
inner: T, inner: T,
} }
#[async_trait]
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
for Receiver<T> for Receiver<T>
{ {
@ -54,7 +58,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
match self.inner.next().await { match self.inner.next().await {
None => Err(Errors::Closed), None => Err(Errors::Closed),
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)), Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))), Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))),
} }
} }
} }

View File

@ -41,7 +41,7 @@ impl Decoder for StreamCodec {
match str::from_utf8(line.as_ref()) { match str::from_utf8(line.as_ref()) {
Ok(s) => Ok(Some(s.to_string())), Ok(s) => Ok(Some(s.to_string())),
Err(_) => Err(io::Error::other("invalid UTF-8")), Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid UTF-8")),
} }
} else { } else {
Ok(None) Ok(None)

View File

@ -1,7 +1,6 @@
use std::io::Error;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT}; use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
use jsonrpsee::core::Error;
use tokio::net::{TcpStream, ToSocketAddrs}; use tokio::net::{TcpStream, ToSocketAddrs};
use tokio_util::codec::Decoder; use tokio_util::codec::Decoder;

View File

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="console-application">
<id>org.asamk.SignalCli</id>
<name>signal-cli</name>
<summary>Use Signal messenger in terminal</summary>
<developer id="org.asamk">
<name>AsamK</name>
</developer>
<icon type="stock">org.asamk.SignalCli</icon>
<keywords>
<keyword>signal</keyword>
<keyword>signal-cli</keyword>
<keyword>messenger</keyword>
<keyword>messaging</keyword>
</keywords>
<url type="bugtracker">https://github.com/AsamK/signal-cli/issues</url>
<url type="homepage">https://github.com/AsamK/signal-cli</url>
<url type="donation">https://github.com/sponsors/AsamK</url>
<url type="faq">https://github.com/AsamK/signal-cli/discussions</url>
<url type="vcs-browser">https://github.com/AsamK/signal-cli</url>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<description>
<p>
signal-cli is an unofficial commandline interface for the Signal Messenger.
It supports many Signal functions, including registering, verifying, sending and receiving messages.
For registering you need a phone number where you can receive SMS or incoming calls.
Alternatively signal-cli can be linked to an existing App account.
</p>
</description>
<categories>
<category>Utility</category>
<category>Java</category>
</categories>
<provides>
<binary>signal-cli</binary>
</provides>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="0.14.7" date="2026-08-01">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.7</url>
</release>
<release version="0.14.6" date="2026-07-12">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.6</url>
</release>
<release version="0.14.5" date="2026-06-11">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.5</url>
</release>
<release version="0.14.4" date="2026-05-23">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
</release>
<release version="0.14.3" date="2026-04-22">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.3</url>
</release>
<release version="0.14.2" date="2026-04-04">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
</release>
<release version="0.14.1" date="2026-03-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.1</url>
</release>
<release version="0.14.0" date="2026-03-01">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.0</url>
</release>
<release version="0.13.24" date="2026-02-05">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.24</url>
</release>
<release version="0.13.23" date="2026-01-24">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.23</url>
</release>
<release version="0.13.22" date="2025-11-14">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.22</url>
</release>
<release version="0.13.21" date="2025-10-25">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.21</url>
</release>
<release version="0.13.20" date="2025-09-23">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.20</url>
</release>
<release version="0.13.19" date="2025-09-15">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.19</url>
</release>
<release version="0.13.18" date="2025-07-16">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.18</url>
</release>
<release version="0.13.17" date="2025-06-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.17</url>
</release>
<release version="0.13.16" date="2025-06-07">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.16</url>
</release>
<release version="0.13.15" date="2025-05-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.15</url>
</release>
<release version="0.13.14" date="2025-04-06">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.14</url>
</release>
<release version="0.13.13" date="2025-02-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.13</url>
</release>
<release version="0.13.12" date="2025-01-18">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.12</url>
</release>
<release version="0.13.11" date="2024-12-26">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.11</url>
</release>
<release version="0.13.10" date="2024-11-30">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.10</url>
</release>
<release version="0.13.9" date="2024-10-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.9</url>
</release>
<release version="0.13.8" date="2024-10-26">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.8</url>
</release>
<release version="0.13.7" date="2024-09-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.7</url>
</release>
<release version="0.13.6" date="2024-09-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.6</url>
</release>
<release version="0.13.5" date="2024-07-25">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.5</url>
</release>
<release version="0.13.4" date="2024-06-06">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.4</url>
</release>
<release version="0.13.3" date="2024-04-19">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.3</url>
</release>
<release version="0.13.2" date="2024-03-23">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.2</url>
</release>
<release version="0.13.1" date="2024-02-27">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.1</url>
</release>
<release version="0.13.0" date="2024-02-18">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.0</url>
</release>
<release version="0.12.8" date="2024-02-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.8</url>
</release>
<release version="0.12.7" date="2023-12-15">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.7</url>
</release>
</releases>
</component>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" version="1.1" viewBox="0 0 33.867 33.867" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-32.279 -138.64)">
<g transform="matrix(.45526 0 0 .45526 33.984 140.17)">
<path d="m33.468 66.938c-18.454 0-33.468-15.014-33.468-33.469s15.014-33.469 33.468-33.469c18.455 0 33.469 15.014 33.469 33.469 0 5.621-1.421 11.161-4.116 16.076l4.608 17.2-16.849-4.516c-5.172 3.084-11.069 4.709-17.112 4.709z" fill="#fff"/>
<path d="m33.468 67.184c-18.454 0-33.468-15.014-33.468-33.469s15.014-33.469 33.468-33.469c18.455 0 33.469 15.014 33.469 33.469 0 5.621-1.421 11.161-4.116 16.076l4.608 17.2-16.849-4.516c-5.172 3.084-11.069 4.709-17.112 4.709zm0-62.938c-16.249 0-29.468 13.22-29.468 29.469s13.219 29.469 29.468 29.469c5.582 0 11.021-1.574 15.729-4.554l0.74-0.468 11.835 3.171-3.243-12.1 0.419-0.72c2.609-4.484 3.988-9.602 3.988-14.799 0-16.248-13.219-29.468-29.468-29.468z"/>
<path d="m25.515 45.296q-2.3937 0-4.2817-0.97772-1.8543-0.97772-2.9332-3.0343-1.0451-2.0566-1.0451-5.2595 0-3.3377 1.1126-5.428 1.1126-2.0903 3.0006-3.068 1.9217-0.97772 4.3492-0.97772 1.3823 0 2.6634 0.30343 1.2812 0.26972 2.0903 0.67429l-0.91029 2.4612q-0.80915-0.30343-1.888-0.57315t-2.0229-0.26972q-5.3269 0-5.3269 6.844 0 3.2703 1.2812 5.0235 1.3149 1.7194 3.8772 1.7194 1.4834 0 2.596-0.30343 1.1463-0.30343 2.0903-0.74172v2.6297q-0.91029 0.472-2.0229 0.708-1.0789 0.26972-2.6297 0.26972zm11.901-0.33714h-2.9669v-25.623h2.9669zm7.2486-24.848q0.67429 0 1.18 0.472 0.53943 0.43829 0.53943 1.416 0 0.94401-0.53943 1.416-0.50572 0.472-1.18 0.472-0.74172 0-1.2474-0.472-0.50572-0.472-0.50572-1.416 0-0.97772 0.50572-1.416 0.50572-0.472 1.2474-0.472zm1.4497 6.7766v18.071h-2.9669v-18.071z" aria-label="cli"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -32,6 +32,8 @@ RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true RestrictNamespaces=true
RestrictRealtime=true RestrictRealtime=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
StandardInput=socket StandardInput=socket
StandardOutput=journal StandardOutput=journal
StandardError=journal StandardError=journal

View File

@ -8,9 +8,11 @@ After=network-online.target
[Service] [Service]
Type=dbus Type=dbus
Environment="SIGNAL_CLI_OPTS=-Xms2m" Environment="SIGNAL_CLI_OPTS=-Xms2m"
ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon --dbus-system ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon --system
User=signal-cli User=signal-cli
BusName=org.asamk.Signal BusName=org.asamk.Signal
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
[Install] [Install]
Alias=dbus-org.asamk.Signal.service Alias=dbus-org.asamk.Signal.service

View File

@ -8,9 +8,11 @@ After=network-online.target
[Service] [Service]
Type=dbus Type=dbus
Environment="SIGNAL_CLI_OPTS=-Xms2m" Environment="SIGNAL_CLI_OPTS=-Xms2m"
ExecStart=%dir%/bin/signal-cli -a %I --config /var/lib/signal-cli daemon --dbus-system ExecStart=%dir%/bin/signal-cli -a %I --config /var/lib/signal-cli daemon --system
User=signal-cli User=signal-cli
BusName=org.asamk.Signal BusName=org.asamk.Signal
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
[Install] [Install]
Alias=dbus-org.asamk.Signal.service Alias=dbus-org.asamk.Signal.service

View File

@ -1,359 +0,0 @@
# Voice Call Support
## Overview
signal-cli supports voice calls by spawning a subprocess called
`signal-call-tunnel` for each call. The tunnel handles WebRTC negotiation and
audio transport. signal-cli communicates with the tunnel over its stdin/stdout
using newline-delimited JSON messages, relaying signaling between the tunnel
and the Signal protocol.
```
signal-cli signal-call-tunnel
| |
|-- spawn --------------------------->|
|-- config JSON on stdin ------------>|
| |
|-- commands on stdin --------------->|
|<-- events on stdout ----------------|
| | WebRTC
| signaling relay | audio I/O
| |
| (stderr: tunnel logging) -------->| (captured by signal-cli)
```
Each call gets its own tunnel process. When the call ends, signal-cli closes
stdin and destroys the process.
Audio device names (`inputDeviceName`, `outputDeviceName`) are opaque strings
returned by the tunnel in its `ready` message. signal-cli passes them through
to JSON-RPC clients, which use them to connect audio via platform APIs.
---
## Spawning the Tunnel
For each call, signal-cli:
1. Spawns `signal-call-tunnel`
2. Writes config JSON followed by a newline to stdin
3. Keeps stdin open for subsequent control messages
4. Reads control events from stdout
5. Captures stderr for logging
The `signal-call-tunnel` binary is located by searching (in order):
1. `SIGNAL_CALL_TUNNEL_BIN` environment variable
2. `<signal-cli install dir>/bin/signal-call-tunnel` (detected from jar location)
3. `signal-call-tunnel` on `PATH`
### Config JSON
The first line written to the tunnel's stdin:
```json
{
"call_id": 12345,
"is_outgoing": true,
"local_device_id": 1,
"input_device_name": "signal_input",
"output_device_name": "signal_output"
}
```
| Field | Type | Description |
|----------------------|-------------------------|-----------------------------------------------|
| `call_id` | unsigned 64-bit integer | Call identifier (use unsigned representation) |
| `is_outgoing` | boolean | Whether this is an outgoing call |
| `local_device_id` | integer | Signal device ID |
| `input_device_name` | string (optional) | Requested input audio device name |
| `output_device_name` | string (optional) | Requested output audio device name |
If `input_device_name` or `output_device_name` are omitted, the tunnel
chooses default names. On Linux, these are per-call unique names (e.g.,
`signal_input_<call_id>`). On macOS, these are the fixed names `signal_input`
and `signal_output`, which must match the pre-installed BlackHole drivers.
---
## Control Protocol
Newline-delimited JSON messages over stdin (signal-cli to tunnel) and stdout
(tunnel to signal-cli). The first line on stdin is the config JSON. Subsequent
lines are control messages.
### signal-cli -> Tunnel (stdin)
| Type | When | Fields |
|----------------------|----------------------------|---------------------------------------------------------------------------------------------------|
| `createOutgoingCall` | Outgoing call setup | `callId`, `peerId` |
| `proceed` | After offer/receivedOffer | `callId`, `hideIp`, `iceServers` |
| `receivedOffer` | Incoming call | `callId`, `peerId`, `opaque`, `age`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
| `receivedAnswer` | Outgoing call answered | `opaque`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
| `receivedIce` | ICE candidates arrive | `candidates` (array of base64 opaque blobs) |
| `accept` | User accepts incoming call | *(none)* |
| `hangup` | End the call | *(none)* |
### Tunnel -> signal-cli (stdout)
| Type | When | Fields |
|---------------|---------------------------------------------|------------------------------------------------------|
| `ready` | Control socket bound, audio devices created | `inputDeviceName`, `outputDeviceName` |
| `sendOffer` | Tunnel generated an offer | `callId`, `opaque`, `callMediaType` |
| `sendAnswer` | Tunnel generated an answer | `callId`, `opaque` |
| `sendIce` | ICE candidates gathered | `callId`, `candidates` (array of `{"opaque":"..."}`) |
| `sendHangup` | Tunnel wants to hang up | `callId`, `hangupType` |
| `sendBusy` | Line is busy | `callId` |
| `stateChange` | Call state transition | `state`, `reason` (optional) |
| `error` | Something went wrong | `message` |
Opaque blobs and identity keys are base64-encoded. ICE servers use the format:
```json
{
"urls": [
"turn:example.com"
],
"username": "u",
"password": "p"
}
```
---
## Startup Sequence
```
signal-cli signal-call-tunnel
| |
|-- spawn process ------------------> |
|-- config JSON + newline on stdin ---->|
| | parse config
| | initialize audio
| |
|<-------- ready (on stdout) -----------|
| {"type":"ready", |
| "inputDeviceName":"...", |
| "outputDeviceName":"..."} |
| |
|-- control messages on stdin --------->|
|<-- control events on stdout ----------|
```
---
## Call Flows
### Outgoing call
```
signal-cli signal-call-tunnel Remote Phone
| | |
|-- spawn + config ------->| |
|<-- ready ----------------| |
|-- createOutgoingCall --->| |
|-- proceed (TURN) ------->| |
| | create offer |
|<-- sendOffer ------------| |
|-- offer via Signal -------------------------------->|
|<-- answer via Signal -------------------------------|
|-- receivedAnswer ------->| (+ identity keys) |
|<-- sendIce --------------| |
|-- ICE via Signal -------------------------------> |
|<-- ICE via Signal -------------------------------- |
|-- receivedIce ---------->| |
| | ICE connects |
|<-- stateChange:Connected | |
```
### Incoming call
```
signal-cli signal-call-tunnel Remote Phone
| | |
|<-- offer via Signal --------------------------------|
|-- spawn + config ------->| |
|<-- ready ----------------| |
|-- receivedOffer -------->| (+ identity keys) |
|-- proceed (TURN) ------->| |
| | process offer |
|<-- sendAnswer -----------| |
|-- answer via Signal -------------------------------->|
|<-- sendIce --------------| |
|-- ICE via Signal ------------------------------> |
|<-- ICE via Signal -------------------------------- |
|-- receivedIce ---------->| |
| | ICE connecting... |
| | |
| (user accepts call) | |
| Java defers accept | |
| | |
|<-- stateChange:Ringing --| (tunnel ready to accept)|
|-- accept --------------->| (deferred accept sent) |
| | accept |
|<-- stateChange:Connected | |
```
### JSON-RPC client perspective
An external application (bot, UI, test script) interacts via JSON-RPC only.
**Important:** Call event notifications are not sent by default. Clients must
call `subscribeCallEvents` before initiating or receiving calls. Without this,
incoming calls are silently ignored (no tunnel is spawned).
```
JSON-RPC Client signal-cli daemon
| |
|-- subscribeCallEvents() ------------>| (required: enables call support)
| |
|-- startCall(recipient) ------------->|
|<-- {callId, state, -|
| inputDeviceName, |
| outputDeviceName} |
| |
|<-- callEvent: RINGING_OUTGOING ------|
| ... remote answers ... |
|<-- callEvent: CONNECTED -------------|
| |
| connect to audio devices |
| (via platform audio APIs) |
| |
|-- hangupCall(callId) --------------->| (or: receive callEvent ENDED)
|<-- callEvent: ENDED -----------------|
| disconnect from audio devices |
```
For incoming calls:
```
JSON-RPC Client signal-cli daemon
| |
|-- subscribeCallEvents() ------------>| (if not already subscribed)
| |
|<-- callEvent: RINGING_INCOMING ------| (includes callId, device names)
| |
|-- acceptCall(callId) --------------->|
|<-- {callId, state, -|
| inputDeviceName, |
| outputDeviceName} |
| |
|<-- callEvent: CONNECTING ------------|
|<-- callEvent: CONNECTED -------------|
| |
| connect to audio devices |
| (via platform audio APIs) |
```
To stop receiving call events, call `unsubscribeCallEvents`.
---
## State Machine
Call states as seen by JSON-RPC clients:
```
startCall()
|
v
+----- RINGING_OUTGOING ----+ RINGING_INCOMING -----+
| | | | |
| (timeout | (answered) | (rejected) | acceptCall() | (timeout
| ~60s) | | | | ~60s)
v v v v v
ENDED CONNECTED ENDED CONNECTING ENDED
| |
| v
| CONNECTED
| |
| (hangup/error) | (hangup/error)
v v
ENDED ENDED
```
For outgoing calls, `CONNECTED` fires directly when the tunnel reports
`Connected` state -- there is no intermediate `CONNECTING` event.
For incoming calls, `CONNECTING` is set by Java when the user calls
`acceptCall()`, before the tunnel completes ICE negotiation.
Both directions have a 60-second ring timeout.
Reconnection (ICE restart):
```
CONNECTED --> RECONNECTING --> CONNECTED (ICE restart succeeded)
|
v
ENDED (ICE restart failed)
```
`RECONNECTING` maps from the tunnel's `Connecting` state, which is emitted
during ICE restarts (not during initial connection).
---
## CallManager.java
`lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java`
Manages the call lifecycle from the Java side:
1. Spawns `signal-call-tunnel` and writes config JSON to stdin
2. Keeps stdin open as the control write channel; reads stdout for control events
3. Captures stderr for tunnel logging
4. Parses `inputDeviceName` and `outputDeviceName` from the tunnel's `ready`
message and includes them in `CallInfo`
5. Translates tunnel state changes into `CallInfo.State` values and fires
`callEvent` JSON-RPC notifications to connected clients
6. Defers the `accept` message for incoming calls until the tunnel reports
`Ringing` state (sending earlier causes the tunnel to drop it)
7. Schedules a 60-second ring timeout for both incoming and outgoing calls
8. On hangup: sends hangup message, closes stdin, and destroys the process
---
## Implementation Notes
### Peer ID consistency
The `peerId` field in `createOutgoingCall` and `receivedOffer` must be the actual
remote peer UUID (e.g., `senderAddress.toString()`). The tunnel rejects ICE
candidates if the peer ID doesn't match across calls, causing "Ignoring
peer-reflexive ICE candidate because the ufrag is unknown."
### sendHangup semantics
`sendHangup` from the tunnel is a request to send a hangup message via Signal
protocol. It is **not** a local state change -- local state transitions come
exclusively from `stateChange` events. For single-device clients, ignore
`AcceptedOnAnotherDevice`, `DeclinedOnAnotherDevice`, and
`BusyOnAnotherDevice` hangup types in the `hangupType` field -- sending these to
the remote peer causes it to terminate the call prematurely.
### Call ID serialization
Call IDs can exceed `Long.MAX_VALUE` in Java. Use `Long.toUnsignedString()` when
serializing to JSON for the tunnel (which expects unsigned 64-bit integers). In
the config JSON, `call_id` should also use unsigned representation.
### Incoming hangup filtering
When receiving hangup messages via Signal protocol, only honor `NORMAL` type
hangups. `ACCEPTED`, `DECLINED`, and `BUSY` types are multi-device coordination
messages and should be ignored by single-device clients.
### JSON-RPC call ID types
JSON-RPC clients may send call IDs as various numeric types (Long, BigInteger,
Integer). Use `Number.longValue()` rather than direct casting when extracting
call IDs from JSON-RPC parameters.
### Identity key format
Identity keys in `senderIdentityKey` and `receiverIdentityKey` must be **raw
32-byte Curve25519 public keys** (without the 0x05 DJB type prefix). If the
33-byte serialized form is used instead, SRTP key derivation produces different
keys on each side, causing authentication failures.

View File

@ -0,0 +1,226 @@
[
{
"name":"[Z"
},
{
"name":"com.sun.security.auth.module.UnixSystem",
"fields":[{"name":"gid"}, {"name":"groups"}, {"name":"uid"}, {"name":"username"}]
},
{
"name":"java.lang.Boolean",
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.Class",
"methods":[{"name":"getCanonicalName","parameterTypes":[] }]
},
{
"name":"java.lang.ClassLoader",
"methods":[{"name":"getPlatformClassLoader","parameterTypes":[] }, {"name":"loadClass","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.IllegalArgumentException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.IllegalStateException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.NoSuchMethodError"
},
{
"name":"java.lang.String"
},
{
"name":"java.lang.Throwable",
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }]
},
{
"name":"java.lang.UnsatisfiedLinkError",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.util.UUID",
"methods":[{"name":"<init>","parameterTypes":["long","long"] }, {"name":"getLeastSignificantBits","parameterTypes":[] }, {"name":"getMostSignificantBits","parameterTypes":[] }]
},
{
"name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"
},
{
"name":"org.asamk.signal.manager.storage.protocol.SignalProtocolStore",
"methods":[{"name":"getIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress"] }, {"name":"getIdentityKeyPair","parameterTypes":[] }, {"name":"getLocalRegistrationId","parameterTypes":[] }, {"name":"isTrustedIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.IdentityKey","org.signal.libsignal.protocol.state.IdentityKeyStore$Direction"] }, {"name":"loadKyberPreKey","parameterTypes":["int"] }, {"name":"loadPreKey","parameterTypes":["int"] }, {"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] }, {"name":"loadSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress"] }, {"name":"loadSignedPreKey","parameterTypes":["int"] }, {"name":"markKyberPreKeyUsed","parameterTypes":["int"] }, {"name":"removePreKey","parameterTypes":["int"] }, {"name":"saveIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.IdentityKey"] }, {"name":"storeSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID","org.signal.libsignal.protocol.groups.state.SenderKeyRecord"] }, {"name":"storeSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.state.SessionRecord"] }]
},
{
"name":"org.asamk.signal.manager.storage.senderKeys.SenderKeyStore",
"methods":[{"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] }, {"name":"storeSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID","org.signal.libsignal.protocol.groups.state.SenderKeyRecord"] }]
},
{
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]
},
{
"name":"org.signal.libsignal.protocol.DuplicateMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.IdentityKey",
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["byte[]"] }, {"name":"serialize","parameterTypes":[] }]
},
{
"name":"org.signal.libsignal.protocol.IdentityKeyPair",
"methods":[{"name":"serialize","parameterTypes":[] }]
},
{
"name":"org.signal.libsignal.protocol.InvalidKeyException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.InvalidKeyIdException"
},
{
"name":"org.signal.libsignal.protocol.InvalidMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.NoSessionException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.SignalProtocolAddress",
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["java.lang.String","int"] }]
},
{
"name":"org.signal.libsignal.protocol.UntrustedIdentityException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.fingerprint.FingerprintParsingException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyRecord",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyStore"
},
{
"name":"org.signal.libsignal.protocol.logging.Log",
"methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.message.PlaintextContent",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.message.PreKeySignalMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.protocol.message.SenderKeyMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.protocol.message.SignalMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore"
},
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
},
{
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.state.KyberPreKeyStore"
},
{
"name":"org.signal.libsignal.protocol.state.PreKeyRecord",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.state.PreKeyStore"
},
{
"name":"org.signal.libsignal.protocol.state.SessionRecord",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["byte[]"] }]
},
{
"name":"org.signal.libsignal.protocol.state.SessionStore"
},
{
"name":"org.signal.libsignal.protocol.state.SignedPreKeyRecord",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.state.SignedPreKeyStore"
},
{
"name":"org.signal.libsignal.usernames.BadNicknameCharacterException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.CannotBeEmptyException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.NicknameTooLongException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.NicknameTooShortException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.zkgroup.InvalidInputException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.sqlite.BusyHandler",
"methods":[{"name":"callback","parameterTypes":["int"] }]
},
{
"name":"org.sqlite.Collation",
"methods":[{"name":"xCompare","parameterTypes":["java.lang.String","java.lang.String"] }]
},
{
"name":"org.sqlite.Function",
"fields":[{"name":"args"}, {"name":"context"}, {"name":"value"}],
"methods":[{"name":"xFunc","parameterTypes":[] }]
},
{
"name":"org.sqlite.Function$Aggregate",
"methods":[{"name":"clone","parameterTypes":[] }, {"name":"xFinal","parameterTypes":[] }, {"name":"xStep","parameterTypes":[] }]
},
{
"name":"org.sqlite.Function$Window",
"methods":[{"name":"xInverse","parameterTypes":[] }, {"name":"xValue","parameterTypes":[] }]
},
{
"name":"org.sqlite.ProgressHandler",
"methods":[{"name":"progress","parameterTypes":[] }]
},
{
"name":"org.sqlite.core.DB",
"methods":[{"name":"onCommit","parameterTypes":["boolean"] }, {"name":"onUpdate","parameterTypes":["int","java.lang.String","java.lang.String","long"] }, {"name":"throwex","parameterTypes":[] }, {"name":"throwex","parameterTypes":["int"] }]
},
{
"name":"org.sqlite.core.DB$ProgressObserver",
"methods":[{"name":"progress","parameterTypes":["int","int"] }]
},
{
"name":"org.sqlite.core.NativeDB",
"fields":[{"name":"busyHandler"}, {"name":"commitListener"}, {"name":"pointer"}, {"name":"progressHandler"}, {"name":"updateListener"}],
"methods":[{"name":"stringToUtf8ByteArray","parameterTypes":["java.lang.String"] }, {"name":"throwex","parameterTypes":["java.lang.String"] }]
}
]

View File

@ -0,0 +1,8 @@
[
{
"type":"agent-extracted",
"classes":[
]
}
]

View File

@ -0,0 +1,23 @@
[
{
"interfaces":["java.sql.Connection"]
},
{
"interfaces":["org.asamk.Signal"]
},
{
"interfaces":["org.asamk.Signal$Configuration"]
},
{
"interfaces":["org.asamk.Signal$Device"]
},
{
"interfaces":["org.asamk.Signal$Group"]
},
{
"interfaces":["org.asamk.SignalControl"]
},
{
"interfaces":["org.freedesktop.dbus.interfaces.DBus"]
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,194 @@
{
"resources":{
"includes":[{
"pattern":"\\QMETA-INF/maven/org.xerial/sqlite-jdbc/pom.properties\\E"
}, {
"pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E"
}, {
"pattern":"\\QMETA-INF/services/java.sql.Driver\\E"
}, {
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader\\E"
}, {
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition\\E"
}, {
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.transport.ITransportProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AG\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AI\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AS\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AT\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AU\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AZ\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BB\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BM\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BO\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BS\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CA\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CH\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CI\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CL\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CN\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CO\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DK\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EC\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ES\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FI\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GB\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HK\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HU\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ID\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IN\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IT\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_JP\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_LV\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MM\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MO\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MX\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MY\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NG\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NL\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PA\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PH\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PL\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RO\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RU\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SA\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SI\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SK\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TH\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UA\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UG\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_VE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_XK\\E"
}, {
"pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\E"
}, {
"pattern":"\\Qkotlin/annotation/annotation.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/collections/collections.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/coroutines/coroutines.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/internal/internal.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/kotlin.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/ranges/ranges.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/reflect/reflect.kotlin_builtins\\E"
}, {
"pattern":"\\Qlibsignal_jni.dylib\\E"
}, {
"pattern":"\\Qlibsignal_jni.so\\E"
}, {
"pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"
}, {
"pattern":"\\Qorg/asamk/signal/manager/config/whisper.store\\E"
}, {
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
}, {
"pattern":"\\Qorg/sqlite/native/Linux/x86_64/libsqlitejdbc.so\\E"
}, {
"pattern":"\\Qsignal_jni.dll\\E"
}, {
"pattern":"\\Qsqlite-jdbc.properties\\E"
}, {
"pattern":"com/google/i18n/phonenumbers/data/.*"
}, {
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/nfc.nrm\\E"
}, {
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/uprops.icu\\E"
}, {
"pattern":"java.base:\\Qsun/net/idn/uidna.spp\\E"
}, {
"pattern":"java.base:\\Qsun/net/www/content-types.properties\\E"
}, {
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
}]},
"bundles":[{
"name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl",
"locales":["", "en", "und"]
}]
}

View File

@ -0,0 +1,8 @@
{
"types":[
],
"lambdaCapturingTypes":[
],
"proxies":[
]
}

View File

@ -1,29 +0,0 @@
[versions]
slf4j = "2.0.18"
coroutines = "1.10.2"
junit = "6.1.2"
micronaut-json-schema = "2.1.0"
micronaut-core = "5.1.10"
signal-service = "2.15.3_unofficial_152"
[libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85"
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.1"
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
zxing = "com.google.zxing:core:3.5.4"
micronaut-json-schema-annotations = { module = "io.micronaut.jsonschema:micronaut-json-schema-annotations", version.ref = "micronaut-json-schema" }
micronaut-json-schema-processor = { module = "io.micronaut.jsonschema:micronaut-json-schema-processor", version.ref = "micronaut-json-schema" }
micronaut-json-schema-generator = { module = "io.micronaut.jsonschema:micronaut-json-schema-generator", version.ref = "micronaut-json-schema" }
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-core" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.6.1"
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
sqlite = "org.xerial:sqlite-jdbc:3.53.2.1"
hikari = "com.zaxxer:HikariCP:7.1.0"
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }

Binary file not shown.

View File

@ -1,9 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

31
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015 the original authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -15,12 +15,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
# gradlew start up script for POSIX generated by Gradle. # Gradle start up script for POSIX generated by Gradle.
# #
# Important for running: # Important for running:
# #
@ -29,7 +27,7 @@
# bash, then to run this script, type that shell name before the whole # bash, then to run this script, type that shell name before the whole
# command line, like: # command line, like:
# #
# ksh gradlew # ksh Gradle
# #
# Busybox and similar reduced shells will NOT work, because this script # Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features: # requires all of these POSIX shell features:
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -86,7 +84,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -114,6 +112,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@ -146,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -154,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -171,6 +170,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@ -202,15 +202,16 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command;
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# and any embedded shellness will be escaped. # shell script including quotes and variable substitutions, so put them in
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # double quotes to make sure that they get re-expanded; and
# treated as '${Hostname}' itself on the command line. # * put everything else in single quotes, so that it's not re-expanded.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

58
gradlew.bat vendored
View File

@ -13,18 +13,16 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem gradlew startup script for Windows @rem Gradle startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled @rem Set local scope for the variables with windows NT shell
setlocal EnableExtensions if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@ -45,13 +43,13 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
"%COMSPEC%" /c exit 1 goto fail
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@ -59,24 +57,36 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
"%COMSPEC%" /c exit 1 goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute gradlew @rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel :end
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts @rem End local scope for the variables with windows NT shell
"%COMSPEC%" /c exit %ERRORLEVEL% if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -4,23 +4,17 @@ plugins {
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_25 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_25 targetCompatibility = JavaVersion.VERSION_17
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
toolchain {
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
}
}
} }
val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString() val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString()
dependencies { dependencies {
if (libsignalClientPath == null) { if (libsignalClientPath == null) {
implementation(libs.signalnetwork) implementation(libs.signalservice)
} else { } else {
implementation(libs.signalnetwork) { implementation(libs.signalservice) {
exclude(group = "org.signal", module = "libsignal-client") exclude(group = "org.signal", module = "libsignal-client")
} }
implementation(files(libsignalClientPath)) implementation(files(libsignalClientPath))
@ -30,11 +24,8 @@ dependencies {
implementation(libs.slf4j.api) implementation(libs.slf4j.api)
implementation(libs.sqlite) implementation(libs.sqlite)
implementation(libs.hikari) implementation(libs.hikari)
compileOnly(libs.kotlinx.coroutines.core)
testImplementation(libs.junit.jupiter) testImplementation(libs.junit)
testImplementation(platform(libs.junit.jupiter.bom))
testRuntimeOnly(libs.junit.launcher)
} }
tasks.named<Test>("test") { tasks.named<Test>("test") {

View File

@ -1,16 +1,9 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.asamk.signal.manager.api.AlreadyReceivingException; import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.CallInfo;
import org.asamk.signal.manager.api.CallOffer;
import org.asamk.signal.manager.api.CaptchaRejectedException;
import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.Configuration; import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.DeviceLimitExceededException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupId;
@ -20,43 +13,32 @@ import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.IdentityVerificationCode; import org.asamk.signal.manager.api.IdentityVerificationCode;
import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.InvalidUsernameException; import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.LastGroupAdminException; import org.asamk.signal.manager.api.LastGroupAdminException;
import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.NotAGroupMemberException; import org.asamk.signal.manager.api.NotAGroupMemberException;
import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException; import org.asamk.signal.manager.api.PendingAdminApprovalException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.ReceiveConfig; import org.asamk.signal.manager.api.ReceiveConfig;
import org.asamk.signal.manager.api.Recipient; import org.asamk.signal.manager.api.Recipient;
import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResult;
import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.StickerPack; import org.asamk.signal.manager.api.StickerPack;
import org.asamk.signal.manager.api.StickerPackId;
import org.asamk.signal.manager.api.StickerPackInvalidException; import org.asamk.signal.manager.api.StickerPackInvalidException;
import org.asamk.signal.manager.api.StickerPackUrl; import org.asamk.signal.manager.api.StickerPackUrl;
import org.asamk.signal.manager.api.TurnServer;
import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl;
import org.asamk.signal.manager.api.UsernameStatus;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.signal.core.util.UuidUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
@ -72,11 +54,7 @@ import java.util.Set;
public interface Manager extends Closeable { public interface Manager extends Closeable {
static boolean isValidNumber(final String e164Number, final String countryCode) { static boolean isValidNumber(final String e164Number, final String countryCode) {
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode); return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
}
static boolean isValidAci(final String aci) {
return UuidUtil.INSTANCE.isUuid(aci);
} }
static boolean isSignalClientAvailable() { static boolean isSignalClientAvailable() {
@ -96,8 +74,6 @@ public interface Manager extends Closeable {
String getSelfNumber(); String getSelfNumber();
String getSelfACI();
/** /**
* This is used for checking a set of phone numbers for registration on Signal * This is used for checking a set of phone numbers for registration on Signal
* *
@ -105,20 +81,13 @@ public interface Manager extends Closeable {
* @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
* @throws IOException if it's unable to get the contacts to check if they're registered * @throws IOException if it's unable to get the contacts to check if they're registered
*/ */
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException; Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException;
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException; void updateAccountAttributes(String deviceName) throws IOException;
void updateAccountAttributes(
String deviceName,
Boolean unrestrictedUnidentifiedSender,
final Boolean discoverableByNumber,
final Boolean numberSharing
) throws IOException;
Configuration getConfiguration(); Configuration getConfiguration();
void updateConfiguration(Configuration configuration) throws NotPrimaryDeviceException; void updateConfiguration(Configuration configuration) throws IOException, NotPrimaryDeviceException;
/** /**
* Update the user's profile. * Update the user's profile.
@ -126,15 +95,11 @@ public interface Manager extends Closeable {
*/ */
void updateProfile(UpdateProfile updateProfile) throws IOException; void updateProfile(UpdateProfile updateProfile) throws IOException;
String getUsername();
UsernameLinkUrl getUsernameLink();
/** /**
* Set a username for the account. * Set a username for the account.
* If the username is null, it will be deleted. * If the username is null, it will be deleted.
*/ */
void setUsername(String username) throws IOException, InvalidUsernameException; String setUsername(String username) throws IOException, InvalidUsernameException;
/** /**
* Set a username for the account. * Set a username for the account.
@ -142,61 +107,34 @@ public interface Manager extends Closeable {
*/ */
void deleteUsername() throws IOException; void deleteUsername() throws IOException;
void startChangeNumber(
String newNumber,
boolean voiceVerification,
String captcha
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException;
void finishChangeNumber(
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
void unregister() throws IOException; void unregister() throws IOException;
void deleteAccount() throws IOException; void deleteAccount() throws IOException;
void submitRateLimitRecaptchaChallenge( void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException;
String challenge,
String captcha
) throws IOException, CaptchaRejectedException;
List<Device> getLinkedDevices() throws IOException; List<Device> getLinkedDevices() throws IOException;
void updateLinkedDevice(int deviceId, String name) throws IOException, NotPrimaryDeviceException; void removeLinkedDevices(int deviceId) throws IOException;
void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException; void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException;
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException;
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException; void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException;
List<Group> getGroups(); List<Group> getGroups();
List<Group> getGroups(Collection<GroupId> groupIds);
SendGroupMessageResults quitGroup( SendGroupMessageResults quitGroup(
GroupId groupId, GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException; ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
void deleteGroup(GroupId groupId) throws IOException; void deleteGroup(GroupId groupId) throws IOException;
SendGroupMessageResults terminateGroup(
GroupId groupId
) throws IOException, GroupNotFoundException, NotAGroupMemberException;
Pair<GroupId, SendGroupMessageResults> createGroup( Pair<GroupId, SendGroupMessageResults> createGroup(
String name, String name, Set<RecipientIdentifier.Single> members, String avatarFile
Set<RecipientIdentifier.Single> members,
String avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException; ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
SendGroupMessageResults updateGroup( SendGroupMessageResults updateGroup(
final GroupId groupId, final GroupId groupId, final UpdateGroup updateGroup
final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException; ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
Pair<GroupId, SendGroupMessageResults> joinGroup( Pair<GroupId, SendGroupMessageResults> joinGroup(
@ -204,42 +142,27 @@ public interface Manager extends Closeable {
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException; ) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
SendMessageResults sendTypingMessage( SendMessageResults sendTypingMessage(
TypingAction action, TypingAction action, Set<RecipientIdentifier> recipients
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds); SendMessageResults sendReadReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
) throws IOException;
SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds); SendMessageResults sendViewedReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
) throws IOException;
SendMessageResults sendMessage( SendMessageResults sendMessage(
Message message, Message message, Set<RecipientIdentifier> recipients
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
SendMessageResults sendEditMessage( SendMessageResults sendEditMessage(
Message message, Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
Set<RecipientIdentifier> recipients,
long editTargetTimestamp
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
/**
* Post a file attachment story to "My Story" or to a group.
*
* @param attachment path to the file to upload and post as a story
* @param allowsReplies whether other users are allowed to reply to this story
* @param groupId if present, post the story to this group instead of "My Story"
*/
SendMessageResults sendStory(
String attachment,
boolean allowsReplies,
Optional<GroupId> groupId
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException;
SendMessageResults sendRemoteDeleteMessage( SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, long targetSentTimestamp, Set<RecipientIdentifier> recipients
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendMessageReaction( SendMessageResults sendMessageReaction(
@ -248,102 +171,36 @@ public interface Manager extends Closeable {
RecipientIdentifier.Single targetAuthor, RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp, long targetSentTimestamp,
Set<RecipientIdentifier> recipients, Set<RecipientIdentifier> recipients,
final boolean notifySelf,
final boolean isStory final boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendAdminDelete(
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set<RecipientIdentifier.Group> recipients,
boolean notifySelf,
boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPinMessage(
int pinDuration,
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set<RecipientIdentifier> recipients,
boolean notifySelf,
boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendUnpinMessage(
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set<RecipientIdentifier> recipients,
boolean notifySelf,
boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPaymentNotificationMessage( SendMessageResults sendPaymentNotificationMessage(
byte[] receipt, byte[] receipt, String note, RecipientIdentifier.Single recipient
String note,
RecipientIdentifier.Single recipient
) throws IOException; ) throws IOException;
void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException; SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
SendMessageResults sendMessageRequestResponse(
MessageEnvelope.Sync.MessageRequestResponse.Type type,
Set<RecipientIdentifier> recipientIdentifiers
);
SendMessageResults sendPollCreateMessage(
final String question,
final boolean allowMultiple,
final List<String> options,
final Set<RecipientIdentifier> recipients,
final boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPollVoteMessage(
final RecipientIdentifier.Single targetAuthor,
final long targetSentTimestamp,
final List<Integer> optionIndexes,
final int voteCount,
final Set<RecipientIdentifier> recipients,
final boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPollTerminateMessage(
final long targetSentTimestamp,
final Set<RecipientIdentifier> recipients,
final boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
void hideRecipient(RecipientIdentifier.Single recipient);
void deleteRecipient(RecipientIdentifier.Single recipient); void deleteRecipient(RecipientIdentifier.Single recipient);
void deleteContact(RecipientIdentifier.Single recipient); void deleteContact(RecipientIdentifier.Single recipient);
void setContactName( void setContactName(
final RecipientIdentifier.Single recipient, RecipientIdentifier.Single recipient, String givenName, final String familyName
final String givenName, ) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) throws UnregisteredRecipientException;
void setContactsBlocked( void setContactsBlocked(
Collection<RecipientIdentifier.Single> recipient, Collection<RecipientIdentifier.Single> recipient, boolean blocked
boolean blocked
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException; ) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
void setGroupsBlocked( void setGroupsBlocked(
Collection<GroupId> groupId, Collection<GroupId> groupId, boolean blocked
boolean blocked
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException; ) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
/** /**
* Change the expiration timer for a contact * Change the expiration timer for a contact
*/ */
void setExpirationTimer( void setExpirationTimer(
RecipientIdentifier.Single recipient, RecipientIdentifier.Single recipient, int messageExpirationTimer
int messageExpirationTimer
) throws IOException, UnregisteredRecipientException; ) throws IOException, UnregisteredRecipientException;
/** /**
@ -382,13 +239,9 @@ public interface Manager extends Closeable {
* Receive new messages from server, returns if no new message arrive in a timespan of timeout. * Receive new messages from server, returns if no new message arrive in a timespan of timeout.
*/ */
void receiveMessages( void receiveMessages(
Optional<Duration> timeout, Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
Optional<Integer> maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException; ) throws IOException, AlreadyReceivingException;
void stopReceiveMessages();
void setReceiveConfig(ReceiveConfig receiveConfig); void setReceiveConfig(ReceiveConfig receiveConfig);
boolean isContactBlocked(RecipientIdentifier.Single recipient); boolean isContactBlocked(RecipientIdentifier.Single recipient);
@ -416,8 +269,7 @@ public interface Manager extends Closeable {
* @param recipient account of the identity * @param recipient account of the identity
*/ */
boolean trustIdentityVerified( boolean trustIdentityVerified(
RecipientIdentifier.Single recipient, RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode
IdentityVerificationCode verificationCode
) throws UnregisteredRecipientException; ) throws UnregisteredRecipientException;
/** /**
@ -433,59 +285,8 @@ public interface Manager extends Closeable {
InputStream retrieveAttachment(final String id) throws IOException; InputStream retrieveAttachment(final String id) throws IOException;
InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException;
InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException;
// --- Voice call methods ---
CallInfo startCall(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
CallInfo acceptCall(long callId) throws IOException;
void hangupCall(long callId) throws IOException;
SendMessageResult rejectCall(long callId) throws IOException;
List<CallInfo> listActiveCalls();
void sendCallOffer(
RecipientIdentifier.Single recipient,
CallOffer offer
) throws IOException, UnregisteredRecipientException;
void sendCallAnswer(
RecipientIdentifier.Single recipient,
long callId,
byte[] answerOpaque
) throws IOException, UnregisteredRecipientException;
void sendIceUpdate(
RecipientIdentifier.Single recipient,
long callId,
List<byte[]> iceCandidates
) throws IOException, UnregisteredRecipientException;
void sendHangup(
RecipientIdentifier.Single recipient,
long callId,
MessageEnvelope.Call.Hangup.Type type
) throws IOException, UnregisteredRecipientException;
void sendBusy(RecipientIdentifier.Single recipient, long callId) throws IOException, UnregisteredRecipientException;
List<TurnServer> getTurnServerInfo() throws IOException;
@Override @Override
void close(); void close() throws IOException;
void addCallEventListener(CallEventListener listener);
void removeCallEventListener(CallEventListener listener);
interface ReceiveMessageHandler { interface ReceiveMessageHandler {
@ -494,9 +295,4 @@ public interface Manager extends Closeable {
void handleMessage(MessageEnvelope envelope, Throwable e); void handleMessage(MessageEnvelope envelope, Throwable e);
} }
interface CallEventListener {
void handleCallEvent(CallInfo callInfo, String reason);
}
} }

View File

@ -1,12 +1,10 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.asamk.signal.manager.internal.LibSignalLogger; import org.asamk.signal.manager.internal.LibSignalLogger;
import org.asamk.signal.manager.internal.SignalLogger;
public class ManagerLogger { public class ManagerLogger {
public static void initLogger() { public static void initLogger() {
LibSignalLogger.initLogger(); LibSignalLogger.initLogger();
SignalLogger.initLogger();
} }
} }

View File

@ -3,10 +3,8 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
@ -14,15 +12,12 @@ import java.io.IOException;
public interface RegistrationManager extends Closeable { public interface RegistrationManager extends Closeable {
void register( void register(
boolean voiceVerification, boolean voiceVerification, String captcha
String captcha, ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException;
final boolean forceRegister
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
void verifyAccount( void verifyAccount(
String verificationCode, String verificationCode, String pin
String pin ) throws IOException, PinLockedException, IncorrectPinException;
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
void deleteLocalAccountData() throws IOException; void deleteLocalAccountData() throws IOException;

View File

@ -4,5 +4,5 @@ import org.asamk.signal.manager.api.TrustNewIdentity;
public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) { public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) {
public static final Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false); public static Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false);
} }

View File

@ -2,7 +2,6 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.AccountCheckException; import org.asamk.signal.manager.api.AccountCheckException;
import org.asamk.signal.manager.api.NotRegisteredException; import org.asamk.signal.manager.api.NotRegisteredException;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
@ -15,7 +14,7 @@ import org.asamk.signal.manager.internal.RegistrationManagerImpl;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.accounts.AccountsStore; import org.asamk.signal.manager.storage.accounts.AccountsStore;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.signal.core.models.ServiceId.ACI; import org.signal.libsignal.protocol.util.KeyHelper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
@ -66,56 +65,29 @@ public class SignalAccountFiles {
} }
public MultiAccountManager initMultiAccountManager() throws IOException { public MultiAccountManager initMultiAccountManager() throws IOException {
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> { final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
try { try {
return new Pair<Manager, Throwable>(initManagerByNumber(a.number(), a.path()), null); return initManager(a.number(), a.path());
} catch (NotRegisteredException e) { } catch (NotRegisteredException | IOException | AccountCheckException e) {
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
return null; return null;
} catch (AccountCheckException | IOException e) { } catch (Throwable e) {
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
return new Pair<Manager, Throwable>(null, e); throw e;
} }
}).filter(Objects::nonNull).toList(); }).filter(Objects::nonNull).toList();
for (final var pair : managerPairs) {
if (pair.second() instanceof IOException e) {
throw e;
}
}
final var managers = managerPairs.stream()
.filter(p -> p != null && p.first() != null)
.map(Pair::first)
.toList();
return new MultiAccountManagerImpl(managers, this); return new MultiAccountManagerImpl(managers, this);
} }
public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException { public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
final var accountPath = accountsStore.getPathByNumber(number); final var accountPath = accountsStore.getPathByNumber(number);
return this.initManagerByNumber(number, accountPath); return this.initManager(number, accountPath);
} }
public Manager initManagerByAci(String aciStr) throws IOException, NotRegisteredException, AccountCheckException { private Manager initManager(
final var aci = ACI.parseOrThrow(aciStr); String number, String accountPath
final var accountPath = accountsStore.getPathByAci(aci);
return this.initManagerByAci(aci, accountPath);
}
private Manager initManagerByNumber(
String number,
String accountPath
) throws IOException, NotRegisteredException, AccountCheckException { ) throws IOException, NotRegisteredException, AccountCheckException {
final var account = loadAccount(accountPath);
if (!number.equals(account.getNumber())) {
account.close();
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
}
return initManagerFromAccount(number, accountPath, account);
}
private SignalAccount loadAccount(final String accountPath) throws NotRegisteredException, IOException {
if (accountPath == null) { if (accountPath == null) {
throw new NotRegisteredException(); throw new NotRegisteredException();
} }
@ -123,27 +95,12 @@ public class SignalAccountFiles {
throw new NotRegisteredException(); throw new NotRegisteredException();
} }
return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings); var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
} if (!number.equals(account.getNumber())) {
private Manager initManagerByAci(
ACI aci,
String accountPath
) throws IOException, NotRegisteredException, AccountCheckException {
final var account = loadAccount(accountPath);
if (!aci.equals(account.getAci())) {
account.close(); account.close();
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci()); throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
} }
return initManagerFromAccount(aci.toString(), accountPath, account);
}
private ManagerImpl initManagerFromAccount(
final String identifier,
final String accountPath,
final SignalAccount account
) throws NotRegisteredException, IOException, AccountCheckException {
if (!account.isRegistered()) { if (!account.isRegistered()) {
account.close(); account.close();
throw new NotRegisteredException(); throw new NotRegisteredException();
@ -165,10 +122,10 @@ public class SignalAccountFiles {
manager.checkAccountState(); manager.checkAccountState();
} catch (DeprecatedVersionException e) { } catch (DeprecatedVersionException e) {
manager.close(); manager.close();
throw new IOException("signal-cli version is too old for the Signal-Server, please update."); throw new AccountCheckException("signal-cli version is too old for the Signal-Server, please update.");
} catch (IOException e) { } catch (IOException e) {
manager.close(); manager.close();
throw new AccountCheckException("Error while checking account " + identifier + ": " + e.getMessage(), e); throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
} }
if (account.getServiceEnvironment() == null) { if (account.getServiceEnvironment() == null) {
@ -196,14 +153,15 @@ public class SignalAccountFiles {
} }
public RegistrationManager initRegistrationManager( public RegistrationManager initRegistrationManager(
String number, String number, Consumer<Manager> newManagerListener
Consumer<Manager> newManagerListener
) throws IOException { ) throws IOException {
final var accountPath = accountsStore.getPathByNumber(number); final var accountPath = accountsStore.getPathByNumber(number);
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) { if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath; final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
var aciIdentityKey = KeyUtils.generateIdentityKeyPair(); var aciIdentityKey = KeyUtils.generateIdentityKeyPair();
var pniIdentityKey = KeyUtils.generateIdentityKeyPair(); var pniIdentityKey = KeyUtils.generateIdentityKeyPair();
var registrationId = KeyHelper.generateRegistrationId(false);
var pniRegistrationId = KeyHelper.generateRegistrationId(false);
var profileKey = KeyUtils.createProfileKey(); var profileKey = KeyUtils.createProfileKey();
var account = SignalAccount.create(pathConfig.dataPath(), var account = SignalAccount.create(pathConfig.dataPath(),
@ -212,9 +170,10 @@ public class SignalAccountFiles {
serviceEnvironment, serviceEnvironment,
aciIdentityKey, aciIdentityKey,
pniIdentityKey, pniIdentityKey,
registrationId,
pniRegistrationId,
profileKey, profileKey,
settings); settings);
account.initDatabase();
return new RegistrationManagerImpl(account, return new RegistrationManagerImpl(account,
pathConfig, pathConfig,
@ -229,7 +188,6 @@ public class SignalAccountFiles {
account.close(); account.close();
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber()); throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
} }
account.initDatabase();
return new RegistrationManagerImpl(account, return new RegistrationManagerImpl(account,
pathConfig, pathConfig,

View File

@ -2,7 +2,7 @@ package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context; import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.core.models.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
public class RenewSessionAction implements HandleAction { public class RenewSessionAction implements HandleAction {
@ -19,8 +19,10 @@ public class RenewSessionAction implements HandleAction {
@Override @Override
public void execute(Context context) throws Throwable { public void execute(Context context) throws Throwable {
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId); context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) {
context.getSendHelper().sendNullMessage(recipientId); context.getSendHelper().sendNullMessage(recipientId);
} }
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {

View File

@ -13,9 +13,7 @@ public class ResendMessageAction implements HandleAction {
private final MessageSendLogEntry messageSendLogEntry; private final MessageSendLogEntry messageSendLogEntry;
public ResendMessageAction( public ResendMessageAction(
final RecipientId recipientId, final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
final long timestamp,
final MessageSendLogEntry messageSendLogEntry
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.timestamp = timestamp; this.timestamp = timestamp;

View File

@ -1,20 +0,0 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
public class RetrieveDeviceNameAction implements HandleAction {
private static final RetrieveDeviceNameAction INSTANCE = new RetrieveDeviceNameAction();
public static RetrieveDeviceNameAction create() {
return INSTANCE;
}
private RetrieveDeviceNameAction() {
}
@Override
public void execute(Context context) throws Throwable {
context.getAccountHelper().refreshDeviceName();
}
}

View File

@ -0,0 +1,20 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
public class RetrieveStorageDataAction implements HandleAction {
private static final RetrieveStorageDataAction INSTANCE = new RetrieveStorageDataAction();
private RetrieveStorageDataAction() {
}
public static RetrieveStorageDataAction create() {
return INSTANCE;
}
@Override
public void execute(Context context) throws Throwable {
context.getStorageHelper().readDataFromStorage();
}
}

View File

@ -15,9 +15,7 @@ public class SendReceiptAction implements HandleAction {
private final List<Long> timestamps = new ArrayList<>(); private final List<Long> timestamps = new ArrayList<>();
public SendReceiptAction( public SendReceiptAction(
final RecipientId recipientId, final RecipientId recipientId, final SignalServiceReceiptMessage.Type type, final long timestamp
final SignalServiceReceiptMessage.Type type,
final long timestamp
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.type = type; this.type = type;

View File

@ -7,6 +7,7 @@ import org.signal.libsignal.metadata.ProtocolException;
import org.signal.libsignal.protocol.message.CiphertextMessage; import org.signal.libsignal.protocol.message.CiphertextMessage;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.internal.push.Envelope; import org.whispersystems.signalservice.internal.push.Envelope;
import java.util.Optional; import java.util.Optional;
@ -14,21 +15,29 @@ import java.util.Optional;
public class SendRetryMessageRequestAction implements HandleAction { public class SendRetryMessageRequestAction implements HandleAction {
private final RecipientId recipientId; private final RecipientId recipientId;
private final ServiceId serviceId;
private final ProtocolException protocolException; private final ProtocolException protocolException;
private final SignalServiceEnvelope envelope; private final SignalServiceEnvelope envelope;
private final ServiceId accountId;
public SendRetryMessageRequestAction( public SendRetryMessageRequestAction(
final RecipientId recipientId, final RecipientId recipientId,
final ServiceId serviceId,
final ProtocolException protocolException, final ProtocolException protocolException,
final SignalServiceEnvelope envelope final SignalServiceEnvelope envelope,
final ServiceId accountId
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.serviceId = serviceId;
this.protocolException = protocolException; this.protocolException = protocolException;
this.envelope = envelope; this.envelope = envelope;
this.accountId = accountId;
} }
@Override @Override
public void execute(Context context) throws Throwable { public void execute(Context context) throws Throwable {
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
int senderDevice = protocolException.getSenderDevice(); int senderDevice = protocolException.getSenderDevice();
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion( Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
protocolException.getGroupId().get())) : Optional.empty(); protocolException.getGroupId().get())) : Optional.empty();
@ -41,9 +50,7 @@ public class SendRetryMessageRequestAction implements HandleAction {
envelopeType = messageContent.getType(); envelopeType = messageContent.getType();
} else { } else {
originalContent = envelope.getContent(); originalContent = envelope.getContent();
envelopeType = envelope.getType() == null envelopeType = envelopeTypeToCiphertextMessageType(envelope.getType());
? CiphertextMessage.WHISPER_TYPE
: envelopeTypeToCiphertextMessageType(envelope.getType());
} }
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent, DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent,
@ -60,7 +67,7 @@ public class SendRetryMessageRequestAction implements HandleAction {
return CiphertextMessage.WHISPER_TYPE; return CiphertextMessage.WHISPER_TYPE;
} }
return switch (type) { return switch (type) {
case PREKEY_MESSAGE -> CiphertextMessage.PREKEY_TYPE; case PREKEY_BUNDLE -> CiphertextMessage.PREKEY_TYPE;
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE; case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE; case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
default -> CiphertextMessage.WHISPER_TYPE; default -> CiphertextMessage.WHISPER_TYPE;

View File

@ -1,21 +0,0 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.jobs.SyncStorageJob;
public class SyncStorageDataAction implements HandleAction {
private static final SyncStorageDataAction INSTANCE = new SyncStorageDataAction();
private SyncStorageDataAction() {
}
public static SyncStorageDataAction create() {
return INSTANCE;
}
@Override
public void execute(Context context) throws Throwable {
context.getJobExecutor().enqueueJob(new SyncStorageJob());
}
}

View File

@ -1,21 +0,0 @@
package org.asamk.signal.manager.api;
public record CallInfo(
long callId,
State state,
RecipientAddress recipient,
String inputDeviceName,
String outputDeviceName,
boolean isOutgoing
) {
public enum State {
IDLE,
RINGING_INCOMING,
RINGING_OUTGOING,
CONNECTING,
CONNECTED,
RECONNECTING,
ENDED
}
}

View File

@ -1,11 +0,0 @@
package org.asamk.signal.manager.api;
public record CallOffer(
long callId, Type type, byte[] opaque
) {
public enum Type {
AUDIO,
VIDEO
}
}

View File

@ -1,16 +0,0 @@
package org.asamk.signal.manager.api;
public class CaptchaRejectedException extends Exception {
public CaptchaRejectedException() {
super("Captcha rejected");
}
public CaptchaRejectedException(final String message) {
super(message);
}
public CaptchaRejectedException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -2,11 +2,11 @@ package org.asamk.signal.manager.api;
public class CaptchaRequiredException extends Exception { public class CaptchaRequiredException extends Exception {
private long nextVerificationAttemptMilliseconds; private long nextAttemptTimestamp;
public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) { public CaptchaRequiredException(final long nextAttemptTimestamp) {
super("Captcha required"); super("Captcha required");
this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds; this.nextAttemptTimestamp = nextAttemptTimestamp;
} }
public CaptchaRequiredException(final String message) { public CaptchaRequiredException(final String message) {
@ -17,7 +17,7 @@ public class CaptchaRequiredException extends Exception {
super(message, cause); super(message, cause);
} }
public long getNextVerificationAttemptMilliseconds() { public long getNextAttemptTimestamp() {
return nextVerificationAttemptMilliseconds; return nextAttemptTimestamp;
} }
} }

View File

@ -2,44 +2,50 @@ package org.asamk.signal.manager.api;
import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.Util;
public record Contact( import java.util.Objects;
String givenName,
String familyName, public class Contact {
String nickName,
String nickNameGivenName, private final String givenName;
String nickNameFamilyName,
String note, private final String familyName;
String color,
int messageExpirationTime, private final String color;
int messageExpirationTimeVersion,
long muteUntil, private final int messageExpirationTime;
boolean hideStory,
boolean isBlocked, private final boolean blocked;
long blockedAt,
boolean isArchived, private final boolean archived;
boolean isProfileSharingEnabled,
boolean isHidden, private final boolean profileSharingEnabled;
Long unregisteredTimestamp
public Contact(
final String givenName,
final String familyName,
final String color,
final int messageExpirationTime,
final boolean blocked,
final boolean archived,
final boolean profileSharingEnabled
) { ) {
this.givenName = givenName;
this.familyName = familyName;
this.color = color;
this.messageExpirationTime = messageExpirationTime;
this.blocked = blocked;
this.archived = archived;
this.profileSharingEnabled = profileSharingEnabled;
}
private Contact(final Builder builder) { private Contact(final Builder builder) {
this(builder.givenName, givenName = builder.givenName;
builder.familyName, familyName = builder.familyName;
builder.nickName, color = builder.color;
builder.nickNameGivenName, messageExpirationTime = builder.messageExpirationTime;
builder.nickNameFamilyName, blocked = builder.blocked;
builder.note, archived = builder.archived;
builder.color, profileSharingEnabled = builder.profileSharingEnabled;
builder.messageExpirationTime,
builder.messageExpirationTimeVersion,
builder.muteUntil,
builder.hideStory,
builder.isBlocked,
builder.blockedAt,
builder.isArchived,
builder.isProfileSharingEnabled,
builder.isHidden,
builder.unregisteredTimestamp);
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -48,23 +54,13 @@ public record Contact(
public static Builder newBuilder(final Contact copy) { public static Builder newBuilder(final Contact copy) {
Builder builder = new Builder(); Builder builder = new Builder();
builder.givenName = copy.givenName(); builder.givenName = copy.getGivenName();
builder.familyName = copy.familyName(); builder.familyName = copy.getFamilyName();
builder.nickName = copy.nickName(); builder.color = copy.getColor();
builder.nickNameGivenName = copy.nickNameGivenName(); builder.messageExpirationTime = copy.getMessageExpirationTime();
builder.nickNameFamilyName = copy.nickNameFamilyName(); builder.blocked = copy.isBlocked();
builder.note = copy.note(); builder.archived = copy.isArchived();
builder.color = copy.color(); builder.profileSharingEnabled = copy.isProfileSharingEnabled();
builder.messageExpirationTime = copy.messageExpirationTime();
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
builder.muteUntil = copy.muteUntil();
builder.hideStory = copy.hideStory();
builder.isBlocked = copy.isBlocked();
builder.blockedAt = copy.blockedAt();
builder.isArchived = copy.isArchived();
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
builder.isHidden = copy.isHidden();
builder.unregisteredTimestamp = copy.unregisteredTimestamp();
return builder; return builder;
} }
@ -83,48 +79,72 @@ public record Contact(
return givenName + " " + familyName; return givenName + " " + familyName;
} }
public String getDisplayNickname() { public String getGivenName() {
final var noNickGivenName = Util.isEmpty(nickNameGivenName); return givenName;
final var noNickFamilyName = Util.isEmpty(nickNameFamilyName);
if (noNickGivenName && noNickFamilyName) {
return null;
} else if (noNickGivenName) {
return nickNameFamilyName;
} else if (noNickFamilyName) {
return nickNameGivenName;
} }
return nickNameGivenName + " " + nickNameFamilyName; public String getFamilyName() {
return familyName;
}
public String getColor() {
return color;
}
public int getMessageExpirationTime() {
return messageExpirationTime;
}
public boolean isBlocked() {
return blocked;
}
public boolean isArchived() {
return archived;
}
public boolean isProfileSharingEnabled() {
return profileSharingEnabled;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Contact contact = (Contact) o;
return messageExpirationTime == contact.messageExpirationTime
&& blocked == contact.blocked
&& archived == contact.archived
&& profileSharingEnabled == contact.profileSharingEnabled
&& Objects.equals(givenName, contact.givenName)
&& Objects.equals(familyName, contact.familyName)
&& Objects.equals(color, contact.color);
}
@Override
public int hashCode() {
return Objects.hash(givenName,
familyName,
color,
messageExpirationTime,
blocked,
archived,
profileSharingEnabled);
} }
public static final class Builder { public static final class Builder {
private String givenName; private String givenName;
private String familyName; private String familyName;
private String nickName;
private String nickNameGivenName;
private String nickNameFamilyName;
private String note;
private String color; private String color;
private int messageExpirationTime; private int messageExpirationTime;
private int messageExpirationTimeVersion = 1; private boolean blocked;
private long muteUntil; private boolean archived;
private boolean hideStory; private boolean profileSharingEnabled;
private boolean isBlocked;
private long blockedAt;
private boolean isArchived;
private boolean isProfileSharingEnabled;
private boolean isHidden;
private Long unregisteredTimestamp;
private Builder() { private Builder() {
} }
public static Builder newBuilder() {
return new Builder();
}
public Builder withGivenName(final String val) { public Builder withGivenName(final String val) {
givenName = val; givenName = val;
return this; return this;
@ -135,26 +155,6 @@ public record Contact(
return this; return this;
} }
public Builder withNickName(final String val) {
nickName = val;
return this;
}
public Builder withNickNameGivenName(final String val) {
nickNameGivenName = val;
return this;
}
public Builder withNickNameFamilyName(final String val) {
nickNameFamilyName = val;
return this;
}
public Builder withNote(final String val) {
note = val;
return this;
}
public Builder withColor(final String val) { public Builder withColor(final String val) {
color = val; color = val;
return this; return this;
@ -165,53 +165,18 @@ public record Contact(
return this; return this;
} }
public Builder withMessageExpirationTimeVersion(final int val) { public Builder withBlocked(final boolean val) {
messageExpirationTimeVersion = val; blocked = val;
return this; return this;
} }
public Builder withMuteUntil(final long val) { public Builder withArchived(final boolean val) {
muteUntil = val; archived = val;
return this; return this;
} }
public Builder withHideStory(final boolean val) { public Builder withProfileSharingEnabled(final boolean val) {
hideStory = val; profileSharingEnabled = val;
return this;
}
public Builder withIsBlocked(final boolean val) {
if (val && !isBlocked) {
blockedAt = System.currentTimeMillis();
} else if (!val) {
blockedAt = 0;
}
isBlocked = val;
return this;
}
public Builder withBlockedAt(final long val) {
blockedAt = val;
return this;
}
public Builder withIsArchived(final boolean val) {
isArchived = val;
return this;
}
public Builder withIsProfileSharingEnabled(final boolean val) {
isProfileSharingEnabled = val;
return this;
}
public Builder withIsHidden(final boolean val) {
isHidden = val;
return this;
}
public Builder withUnregisteredTimestamp(final Long val) {
unregisteredTimestamp = val;
return this; return this;
} }

View File

@ -1,12 +0,0 @@
package org.asamk.signal.manager.api;
public class DeviceLimitExceededException extends Exception {
public DeviceLimitExceededException(final String message) {
super(message);
}
public DeviceLimitExceededException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import java.net.URI; import java.net.URI;
@ -36,7 +37,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
} }
ECPublicKey deviceKey; ECPublicKey deviceKey;
try { try {
deviceKey = new ECPublicKey(publicKeyBytes); deviceKey = Curve.decodePoint(publicKeyBytes, 0);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new InvalidDeviceLinkException("Invalid device link", e); throw new InvalidDeviceLinkException("Invalid device link", e);
} }

View File

@ -12,9 +12,10 @@ public record Group(
String title, String title,
String description, String description,
GroupInviteLinkUrl groupInviteLinkUrl, GroupInviteLinkUrl groupInviteLinkUrl,
Set<GroupMember> members, Set<RecipientAddress> members,
Set<RecipientAddress> pendingMembers, Set<RecipientAddress> pendingMembers,
Set<RecipientAddress> requestingMembers, Set<RecipientAddress> requestingMembers,
Set<RecipientAddress> adminMembers,
Set<RecipientAddress> bannedMembers, Set<RecipientAddress> bannedMembers,
boolean isBlocked, boolean isBlocked,
int messageExpirationTimer, int messageExpirationTimer,
@ -22,14 +23,11 @@ public record Group(
GroupPermission permissionEditDetails, GroupPermission permissionEditDetails,
GroupPermission permissionSendMessage, GroupPermission permissionSendMessage,
boolean isMember, boolean isMember,
boolean isAdmin, boolean isAdmin
boolean isTerminated
) { ) {
public static Group from( public static Group from(
final GroupInfo groupInfo, final GroupInfo groupInfo, final RecipientAddressResolver recipientStore, final RecipientId selfRecipientId
final RecipientAddressResolver recipientStore,
final RecipientId selfRecipientId
) { ) {
return new Group(groupInfo.getGroupId(), return new Group(groupInfo.getGroupId(),
groupInfo.getTitle(), groupInfo.getTitle(),
@ -37,7 +35,8 @@ public record Group(
groupInfo.getGroupInviteLink(), groupInfo.getGroupInviteLink(),
groupInfo.getMembers() groupInfo.getMembers()
.stream() .stream()
.map(m -> org.asamk.signal.manager.api.GroupMember.from(m, recipientStore)) .map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()), .collect(Collectors.toSet()),
groupInfo.getPendingMembers() groupInfo.getPendingMembers()
.stream() .stream()
@ -49,6 +48,11 @@ public record Group(
.map(recipientStore::resolveRecipientAddress) .map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress) .map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()), .collect(Collectors.toSet()),
groupInfo.getAdminMembers()
.stream()
.map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()),
groupInfo.getBannedMembers() groupInfo.getBannedMembers()
.stream() .stream()
.map(recipientStore::resolveRecipientAddress) .map(recipientStore::resolveRecipientAddress)
@ -60,7 +64,6 @@ public record Group(
groupInfo.getPermissionEditDetails(), groupInfo.getPermissionEditDetails(),
groupInfo.getPermissionSendMessage(), groupInfo.getPermissionSendMessage(),
groupInfo.isMember(selfRecipientId), groupInfo.isMember(selfRecipientId),
groupInfo.isAdmin(selfRecipientId), groupInfo.isAdmin(selfRecipientId));
groupInfo.isTerminated());
} }
} }

View File

@ -1,11 +1,11 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupLinkPassword; import org.asamk.signal.manager.groups.GroupLinkPassword;
import org.signal.core.util.Base64;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.storageservice.storage.protos.groups.GroupInviteLink; import org.signal.storageservice.protos.groups.GroupInviteLink;
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.whispersystems.util.Base64UrlSafe;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -39,21 +39,21 @@ public final class GroupInviteLinkUrl {
} }
try { try {
if (!"/".equals(uri.getPath()) && !uri.getPath().isEmpty()) { if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) {
throw new InvalidGroupLinkException("No path was expected in uri"); throw new InvalidGroupLinkException("No path was expected in uri");
} }
var encoding = uri.getFragment(); var encoding = uri.getFragment();
if (encoding == null || encoding.isEmpty()) { if (encoding == null || encoding.length() == 0) {
throw new InvalidGroupLinkException("No reference was in the uri"); throw new InvalidGroupLinkException("No reference was in the uri");
} }
var bytes = Base64.decode(encoding); var bytes = Base64UrlSafe.decodePaddingAgnostic(encoding);
GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes); GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes);
if (groupInviteLink.contentsV1 != null) { if (groupInviteLink.v1Contents != null) {
var groupInviteLinkContentsV1 = groupInviteLink.contentsV1; var groupInviteLinkContentsV1 = groupInviteLink.v1Contents;
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray()); var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray());
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray()); var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray());
@ -90,12 +90,12 @@ public final class GroupInviteLinkUrl {
} }
private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) { private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
var groupInviteLink = new GroupInviteLink.Builder().contentsV1(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey( var groupInviteLink = new GroupInviteLink.Builder().v1Contents(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey(
ByteString.of(groupMasterKey.serialize())) ByteString.of(groupMasterKey.serialize()))
.inviteLinkPassword(ByteString.of(password.serialize())) .inviteLinkPassword(ByteString.of(password.serialize()))
.build()).build(); .build()).build();
var encoding = Base64.encodeUrlSafeWithoutPadding(groupInviteLink.encode()); var encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.encode());
return GROUP_URL_PREFIX + encoding; return GROUP_URL_PREFIX + encoding;
} }
@ -112,7 +112,7 @@ public final class GroupInviteLinkUrl {
return password; return password;
} }
public static final class InvalidGroupLinkException extends Exception { public final static class InvalidGroupLinkException extends Exception {
public InvalidGroupLinkException(String message) { public InvalidGroupLinkException(String message) {
super(message); super(message);
@ -123,7 +123,7 @@ public final class GroupInviteLinkUrl {
} }
} }
public static final class UnknownGroupLinkVersionException extends Exception { public final static class UnknownGroupLinkVersionException extends Exception {
public UnknownGroupLinkVersionException(String message) { public UnknownGroupLinkVersionException(String message) {
super(message); super(message);

View File

@ -1,14 +0,0 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.groups.GroupMemberInfo;
public record GroupMember(
RecipientAddress recipientAddress, boolean isAdmin, String labelEmoji, String label
) {
public static GroupMember from(final GroupMemberInfo memberInfo, final RecipientAddressResolver recipientStore) {
return new GroupMember(recipientStore.resolveRecipientAddress(memberInfo.getRecipientId())
.toApiRecipientAddress(), memberInfo.isAdmin(), memberInfo.labelEmoji(), memberInfo.labelString());
}
}

View File

@ -1,10 +1,17 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.signal.libsignal.protocol.IdentityKey;
public record Identity( public record Identity(
RecipientAddress recipient, RecipientAddress recipient,
byte[] fingerprint, IdentityKey identityKey,
String safetyNumber, String safetyNumber,
byte[] scannableSafetyNumber, byte[] scannableSafetyNumber,
TrustLevel trustLevel, TrustLevel trustLevel,
long dateAddedTimestamp long dateAddedTimestamp
) {} ) {
public byte[] getFingerprint() {
return identityKey.getPublicKey().serialize();
}
}

View File

@ -2,10 +2,6 @@ package org.asamk.signal.manager.api;
public class InvalidNumberException extends Exception { public class InvalidNumberException extends Exception {
public InvalidNumberException(String message) {
super(message);
}
InvalidNumberException(String message, Throwable e) { InvalidNumberException(String message, Throwable e) {
super(message, e); super(message, e);
} }

View File

@ -6,15 +6,12 @@ import java.util.Optional;
public record Message( public record Message(
String messageText, String messageText,
List<String> attachments, List<String> attachments,
boolean viewOnce,
boolean voiceNote,
List<Mention> mentions, List<Mention> mentions,
Optional<Quote> quote, Optional<Quote> quote,
Optional<Sticker> sticker, Optional<Sticker> sticker,
List<Preview> previews, List<Preview> previews,
Optional<StoryReply> storyReply, Optional<StoryReply> storyReply,
List<TextStyle> textStyles, List<TextStyle> textStyles
boolean urgent
) { ) {
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {} public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
@ -24,12 +21,8 @@ public record Message(
RecipientIdentifier.Single author, RecipientIdentifier.Single author,
String message, String message,
List<Mention> mentions, List<Mention> mentions,
List<TextStyle> textStyles, List<TextStyle> textStyles
List<Attachment> attachments ) {}
) {
public record Attachment(String contentType, String filename, String preview) {}
}
public record Sticker(byte[] packId, int stickerId) {} public record Sticker(byte[] packId, int stickerId) {}

View File

@ -3,7 +3,6 @@ package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.helper.RecipientAddressResolver; import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.util.MimeUtils;
import org.signal.libsignal.metadata.ProtocolException; import org.signal.libsignal.metadata.ProtocolException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@ -35,9 +34,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -116,35 +113,17 @@ public record MessageEnvelope(
Optional<Long> remoteDeleteId, Optional<Long> remoteDeleteId,
Optional<Sticker> sticker, Optional<Sticker> sticker,
List<SharedContact> sharedContacts, List<SharedContact> sharedContacts,
Optional<PollCreate> pollCreate,
Optional<PollVote> pollVote,
Optional<PollTerminate> pollTerminate,
List<Mention> mentions, List<Mention> mentions,
List<Preview> previews, List<Preview> previews,
List<TextStyle> textStyles, List<TextStyle> textStyles
Optional<PinMessage> pinMessage,
Optional<UnpinMessage> unpinMessage,
Optional<AdminDelete> adminDelete
) { ) {
static Data from( static Data from(
final SignalServiceDataMessage dataMessage, final SignalServiceDataMessage dataMessage,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider final AttachmentFileProvider fileProvider
) { ) {
var body = dataMessage.getBody();
if (dataMessage.getAttachments().isPresent()) {
for (final var attachment : dataMessage.getAttachments().get()) {
if (MimeUtils.LONG_TEXT.equals(attachment.getContentType()) && attachment.isPointer()) {
final var longBody = longTexts.get(attachment.asPointer().getRemoteId().toString());
if (longBody != null) {
body = Optional.of(longBody);
}
}
}
}
return new Data(dataMessage.getTimestamp(), return new Data(dataMessage.getTimestamp(),
dataMessage.getGroupContext().map(GroupContext::from), dataMessage.getGroupContext().map(GroupContext::from),
dataMessage.getStoryContext() dataMessage.getStoryContext()
@ -152,17 +131,15 @@ public record MessageEnvelope(
recipientResolver, recipientResolver,
addressResolver)), addressResolver)),
dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from), dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
body, dataMessage.getBody(),
dataMessage.getExpiresInSeconds(), dataMessage.getExpiresInSeconds(),
dataMessage.isExpirationUpdate(), dataMessage.isExpirationUpdate(),
dataMessage.isViewOnce(), dataMessage.isViewOnce(),
false, dataMessage.isEndSession(),
dataMessage.isProfileKeyUpdate(), dataMessage.isProfileKeyUpdate(),
dataMessage.getProfileKey().isPresent(), dataMessage.getProfileKey().isPresent(),
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)), dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
dataMessage.getQuote() dataMessage.getQuote().map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
.filter(q -> q.getAuthor() != null && q.getAuthor().isValid())
.map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null), dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null),
dataMessage.getAttachments() dataMessage.getAttachments()
.map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList()) .map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
@ -174,9 +151,6 @@ public record MessageEnvelope(
.map(sharedContact -> SharedContact.from(sharedContact, fileProvider)) .map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
.toList()) .toList())
.orElse(List.of()), .orElse(List.of()),
dataMessage.getPollCreate().map(PollCreate::from),
dataMessage.getPollVote().map(p -> PollVote.from(p, recipientResolver, addressResolver)),
dataMessage.getPollTerminate().map(PollTerminate::from),
dataMessage.getMentions() dataMessage.getMentions()
.map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList()) .map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
.orElse(List.of()), .orElse(List.of()),
@ -185,10 +159,7 @@ public record MessageEnvelope(
.orElse(List.of()), .orElse(List.of()),
dataMessage.getBodyRanges() dataMessage.getBodyRanges()
.map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList()) .map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList())
.orElse(List.of()), .orElse(List.of()));
dataMessage.getPinnedMessage().map(p -> PinMessage.from(p, recipientResolver, addressResolver)),
dataMessage.getUnpinnedMessage().map(p -> UnpinMessage.from(p, recipientResolver, addressResolver)),
dataMessage.getAdminDelete().map(p -> AdminDelete.from(p, recipientResolver, addressResolver)));
} }
public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) { public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
@ -263,7 +234,7 @@ public record MessageEnvelope(
return new Quote(quote.getId(), return new Quote(quote.getId(),
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor())) addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor()))
.toApiRecipientAddress(), .toApiRecipientAddress(),
Optional.of(quote.getText()), Optional.ofNullable(quote.getText()),
quote.getMentions() == null quote.getMentions() == null
? List.of() ? List.of()
: quote.getMentions() : quote.getMentions()
@ -320,9 +291,9 @@ public record MessageEnvelope(
boolean isBorderless boolean isBorderless
) { ) {
static Attachment from(SignalServiceAttachment signalAttachment, AttachmentFileProvider fileProvider) { static Attachment from(SignalServiceAttachment attachment, AttachmentFileProvider fileProvider) {
if (signalAttachment.isPointer()) { if (attachment.isPointer()) {
final var a = signalAttachment.asPointer(); final var a = attachment.asPointer();
final var attachmentFile = fileProvider.getFile(a); final var attachmentFile = fileProvider.getFile(a);
return new Attachment(Optional.of(attachmentFile.getName()), return new Attachment(Optional.of(attachmentFile.getName()),
Optional.of(attachmentFile), Optional.of(attachmentFile),
@ -339,9 +310,8 @@ public record MessageEnvelope(
a.isGif(), a.isGif(),
a.isBorderless()); a.isBorderless());
} else { } else {
Attachment attachment = null; final var a = attachment.asStream();
try (final var a = signalAttachment.asStream()) { return new Attachment(Optional.empty(),
attachment = new Attachment(Optional.empty(),
Optional.empty(), Optional.empty(),
a.getFileName(), a.getFileName(),
a.getContentType(), a.getContentType(),
@ -355,16 +325,11 @@ public record MessageEnvelope(
a.getVoiceNote(), a.getVoiceNote(),
a.isGif(), a.isGif(),
a.isBorderless()); a.isBorderless());
return attachment;
} catch (IOException e) {
return attachment;
}
} }
} }
static Attachment from( static Attachment from(
SignalServiceDataMessage.Quote.QuotedAttachment a, SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider
final AttachmentFileProvider fileProvider
) { ) {
return new Attachment(Optional.empty(), return new Attachment(Optional.empty(),
Optional.empty(), Optional.empty(),
@ -416,7 +381,7 @@ public record MessageEnvelope(
} }
public record Name( public record Name(
Optional<String> nickname, Optional<String> display,
Optional<String> given, Optional<String> given,
Optional<String> family, Optional<String> family,
Optional<String> prefix, Optional<String> prefix,
@ -425,7 +390,7 @@ public record MessageEnvelope(
) { ) {
static Name from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Name name) { static Name from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Name name) {
return new Name(name.getNickname(), return new Name(name.getDisplay(),
name.getGiven(), name.getGiven(),
name.getFamily(), name.getFamily(),
name.getPrefix(), name.getPrefix(),
@ -507,15 +472,15 @@ public record MessageEnvelope(
) { ) {
static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) { static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) {
return new Address(Type.from(address.getType()), return new Address(Address.Type.from(address.getType()),
address.getLabel(), address.getLabel(),
address.getStreet(), address.getLabel(),
address.getPobox(), address.getLabel(),
address.getNeighborhood(), address.getLabel(),
address.getCity(), address.getLabel(),
address.getRegion(), address.getLabel(),
address.getPostcode(), address.getLabel(),
address.getCountry()); address.getLabel());
} }
public enum Type { public enum Type {
@ -534,47 +499,11 @@ public record MessageEnvelope(
} }
} }
public record PollCreate(
String question, boolean allowMultiple, List<String> options
) {
static PollCreate from(
SignalServiceDataMessage.PollCreate pollCreate
) {
return new PollCreate(pollCreate.getQuestion(), pollCreate.getAllowMultiple(), pollCreate.getOptions());
}
}
public record PollVote(
RecipientAddress targetAuthor, long targetSentTimestamp, List<Integer> optionIndexes, int voteCount
) {
static PollVote from(
SignalServiceDataMessage.PollVote pollVote,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new PollVote(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(pollVote.getTargetAuthor()))
.toApiRecipientAddress(),
pollVote.getTargetSentTimestamp(),
pollVote.getOptionIndexes(),
pollVote.getVoteCount());
}
}
public record PollTerminate(long targetSentTimestamp) {
static PollTerminate from(SignalServiceDataMessage.PollTerminate pollTerminate) {
return new PollTerminate(pollTerminate.getTargetSentTimestamp());
}
}
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) { public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) { static Preview from(
SignalServicePreview preview, final AttachmentFileProvider fileProvider
) {
return new Preview(preview.getTitle(), return new Preview(preview.getTitle(),
preview.getDescription(), preview.getDescription(),
preview.getDate(), preview.getDate(),
@ -583,68 +512,18 @@ public record MessageEnvelope(
} }
} }
public record PinMessage(
RecipientAddress targetAuthor, long targetSentTimestamp, long pinDurationSeconds
) {
static PinMessage from(
SignalServiceDataMessage.PinnedMessage pinnedMessage,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new PinMessage(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
pinnedMessage.getTargetAuthor())).toApiRecipientAddress(),
pinnedMessage.getTargetSentTimestamp(),
Boolean.TRUE.equals(pinnedMessage.getForever())
? -1
: pinnedMessage.getPinDurationInSeconds() == null
? 0
: pinnedMessage.getPinDurationInSeconds());
}
}
public record UnpinMessage(RecipientAddress targetAuthor, long targetSentTimestamp) {
static UnpinMessage from(
SignalServiceDataMessage.UnpinnedMessage unpinnedMessage,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new UnpinMessage(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
unpinnedMessage.getTargetAuthor())).toApiRecipientAddress(),
unpinnedMessage.getTargetSentTimestamp());
}
}
public record AdminDelete(RecipientAddress targetAuthor, long targetSentTimestamp) {
static AdminDelete from(
SignalServiceDataMessage.AdminDelete adminDelete,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new AdminDelete(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
adminDelete.getTargetAuthor())).toApiRecipientAddress(), adminDelete.getTargetSentTimestamp());
}
}
} }
public record Edit(long targetSentTimestamp, Data dataMessage) { public record Edit(long targetSentTimestamp, Data dataMessage) {
public static Edit from( public static Edit from(
final SignalServiceEditMessage editMessage, final SignalServiceEditMessage editMessage,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider final AttachmentFileProvider fileProvider
) { ) {
return new Edit(editMessage.getTargetSentTimestamp(), return new Edit(editMessage.getTargetSentTimestamp(),
Data.from(editMessage.getDataMessage(), Data.from(editMessage.getDataMessage(), recipientResolver, addressResolver, fileProvider));
longTexts,
recipientResolver,
addressResolver,
fileProvider));
} }
} }
@ -661,13 +540,12 @@ public record MessageEnvelope(
public static Sync from( public static Sync from(
final SignalServiceSyncMessage syncMessage, final SignalServiceSyncMessage syncMessage,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider final AttachmentFileProvider fileProvider
) { ) {
return new Sync(syncMessage.getSent() return new Sync(syncMessage.getSent()
.map(s -> Sent.from(s, longTexts, recipientResolver, addressResolver, fileProvider)), .map(s -> Sent.from(s, recipientResolver, addressResolver, fileProvider)),
syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)), syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)),
syncMessage.getRead() syncMessage.getRead()
.map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList()) .map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
@ -696,7 +574,6 @@ public record MessageEnvelope(
static Sent from( static Sent from(
SentTranscriptMessage sentMessage, SentTranscriptMessage sentMessage,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider final AttachmentFileProvider fileProvider
@ -712,17 +589,9 @@ public record MessageEnvelope(
.toApiRecipientAddress()) .toApiRecipientAddress())
.collect(Collectors.toSet()), .collect(Collectors.toSet()),
sentMessage.getDataMessage() sentMessage.getDataMessage()
.map(message -> Data.from(message, .map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)),
longTexts,
recipientResolver,
addressResolver,
fileProvider)),
sentMessage.getEditMessage() sentMessage.getEditMessage()
.map(message -> Edit.from(message, .map(message -> Edit.from(message, recipientResolver, addressResolver, fileProvider)),
longTexts,
recipientResolver,
addressResolver,
fileProvider)),
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider))); sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
} }
} }
@ -734,15 +603,11 @@ public record MessageEnvelope(
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver RecipientAddressResolver addressResolver
) { ) {
return new Blocked(blockedListMessage.individuals.stream() return new Blocked(blockedListMessage.getAddresses()
.map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(), .stream()
null, .map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
d.getE164(), .toApiRecipientAddress())
null)) .toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
.toList(),
blockedListMessage.groups.stream()
.map(group -> GroupId.unknownVersion(group.getGroupId()))
.toList());
} }
} }
@ -753,7 +618,7 @@ public record MessageEnvelope(
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver RecipientAddressResolver addressResolver
) { ) {
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSenderAci())) return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
.toApiRecipientAddress(), readMessage.getTimestamp()); .toApiRecipientAddress(), readMessage.getTimestamp());
} }
} }
@ -816,9 +681,7 @@ public record MessageEnvelope(
DELETE, DELETE,
BLOCK, BLOCK,
BLOCK_AND_DELETE, BLOCK_AND_DELETE,
UNBLOCK_AND_ACCEPT, UNBLOCK_AND_ACCEPT;
SPAM,
BLOCK_AND_SPAM;
static Type from(MessageRequestResponseMessage.Type type) { static Type from(MessageRequestResponseMessage.Type type) {
return switch (type) { return switch (type) {
@ -828,8 +691,6 @@ public record MessageEnvelope(
case BLOCK -> BLOCK; case BLOCK -> BLOCK;
case BLOCK_AND_DELETE -> BLOCK_AND_DELETE; case BLOCK_AND_DELETE -> BLOCK_AND_DELETE;
case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT; case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT;
case SPAM -> SPAM;
case BLOCK_AND_SPAM -> BLOCK_AND_SPAM;
}; };
} }
} }
@ -846,6 +707,7 @@ public record MessageEnvelope(
Optional<Busy> busy, Optional<Busy> busy,
List<IceUpdate> iceUpdate, List<IceUpdate> iceUpdate,
Optional<Opaque> opaque, Optional<Opaque> opaque,
boolean isMultiRing,
boolean isUrgent boolean isUrgent
) { ) {
@ -861,13 +723,17 @@ public record MessageEnvelope(
.map(m -> m.stream().map(IceUpdate::from).toList()) .map(m -> m.stream().map(IceUpdate::from).toList())
.orElse(List.of()), .orElse(List.of()),
callMessage.getOpaqueMessage().map(Opaque::from), callMessage.getOpaqueMessage().map(Opaque::from),
callMessage.isMultiRing(),
callMessage.isUrgent()); callMessage.isUrgent());
} }
public record Offer(long id, Type type, byte[] opaque) { public record Offer(long id, String sdp, Type type, byte[] opaque) {
static Offer from(OfferMessage offerMessage) { static Offer from(OfferMessage offerMessage) {
return new Offer(offerMessage.getId(), Type.from(offerMessage.getType()), offerMessage.getOpaque()); return new Offer(offerMessage.getId(),
offerMessage.getSdp(),
Type.from(offerMessage.getType()),
offerMessage.getOpaque());
} }
public enum Type { public enum Type {
@ -883,10 +749,10 @@ public record MessageEnvelope(
} }
} }
public record Answer(long id, byte[] opaque) { public record Answer(long id, String sdp, byte[] opaque) {
static Answer from(AnswerMessage answerMessage) { static Answer from(AnswerMessage answerMessage) {
return new Answer(answerMessage.getId(), answerMessage.getOpaque()); return new Answer(answerMessage.getId(), answerMessage.getSdp(), answerMessage.getOpaque());
} }
} }
@ -924,10 +790,10 @@ public record MessageEnvelope(
} }
} }
public record IceUpdate(long id, byte[] opaque) { public record IceUpdate(long id, String sdp, byte[] opaque) {
static IceUpdate from(IceUpdateMessage iceUpdateMessage) { static IceUpdate from(IceUpdateMessage iceUpdateMessage) {
return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getOpaque()); return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getSdp(), iceUpdateMessage.getOpaque());
} }
} }
@ -958,7 +824,9 @@ public record MessageEnvelope(
Optional<TextAttachment> textAttachment Optional<TextAttachment> textAttachment
) { ) {
public static Story from(SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider) { public static Story from(
SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider
) {
return new Story(storyMessage.getAllowsReplies().orElse(false), return new Story(storyMessage.getAllowsReplies().orElse(false),
storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())), storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)), storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
@ -976,8 +844,7 @@ public record MessageEnvelope(
) { ) {
static TextAttachment from( static TextAttachment from(
SignalServiceTextAttachment textAttachment, SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider
final AttachmentFileProvider fileProvider
) { ) {
return new TextAttachment(textAttachment.getText(), return new TextAttachment(textAttachment.getText(),
textAttachment.getStyle().map(Style::from), textAttachment.getStyle().map(Style::from),
@ -1024,15 +891,13 @@ public record MessageEnvelope(
public static MessageEnvelope from( public static MessageEnvelope from(
SignalServiceEnvelope envelope, SignalServiceEnvelope envelope,
SignalServiceContent content, SignalServiceContent content,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider, final AttachmentFileProvider fileProvider,
Exception exception Exception exception
) { ) {
final var serviceId = envelope.getSourceServiceId(); final var source = !envelope.isUnidentifiedSender() && envelope.hasSourceServiceId()
final var source = !envelope.isUnidentifiedSender() && serviceId != null ? recipientResolver.resolveRecipient(envelope.getSourceAddress())
? recipientResolver.resolveRecipient(serviceId)
: envelope.isUnidentifiedSender() && content != null : envelope.isUnidentifiedSender() && content != null
? recipientResolver.resolveRecipient(content.getSender()) ? recipientResolver.resolveRecipient(content.getSender())
: exception instanceof ProtocolException e : exception instanceof ProtocolException e
@ -1055,15 +920,9 @@ public record MessageEnvelope(
receipt = content.getReceiptMessage().map(Receipt::from); receipt = content.getReceiptMessage().map(Receipt::from);
typing = content.getTypingMessage().map(Typing::from); typing = content.getTypingMessage().map(Typing::from);
data = content.getDataMessage() data = content.getDataMessage()
.map(dataMessage -> Data.from(dataMessage, .map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider));
longTexts, edit = content.getEditMessage().map(s -> Edit.from(s, recipientResolver, addressResolver, fileProvider));
recipientResolver, sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider));
addressResolver,
fileProvider));
edit = content.getEditMessage()
.map(s -> Edit.from(s, longTexts, recipientResolver, addressResolver, fileProvider));
sync = content.getSyncMessage()
.map(s -> Sync.from(s, longTexts, recipientResolver, addressResolver, fileProvider));
call = content.getCallMessage().map(Call::from); call = content.getCallMessage().map(Call::from);
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider)); story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
} else { } else {

View File

@ -3,16 +3,5 @@ package org.asamk.signal.manager.api;
public enum PhoneNumberSharingMode { public enum PhoneNumberSharingMode {
EVERYBODY, EVERYBODY,
CONTACTS, CONTACTS,
NOBODY; NOBODY,
public static PhoneNumberSharingMode valueOfOrNull(String value) {
if (value == null) {
return null;
}
try {
return valueOf(value);
} catch (IllegalArgumentException ignored) {
return null;
}
}
} }

View File

@ -1,3 +0,0 @@
package org.asamk.signal.manager.api;
public class PinLockMissingException extends Exception {}

View File

@ -26,8 +26,6 @@ public class Profile {
private final Set<Capability> capabilities; private final Set<Capability> capabilities;
private final PhoneNumberSharingMode phoneNumberSharingMode;
public Profile( public Profile(
final long lastUpdateTimestamp, final long lastUpdateTimestamp,
final String givenName, final String givenName,
@ -37,8 +35,7 @@ public class Profile {
final String avatarUrlPath, final String avatarUrlPath,
final byte[] mobileCoinAddress, final byte[] mobileCoinAddress,
final UnidentifiedAccessMode unidentifiedAccessMode, final UnidentifiedAccessMode unidentifiedAccessMode,
final Set<Capability> capabilities, final Set<Capability> capabilities
final PhoneNumberSharingMode phoneNumberSharingMode
) { ) {
this.lastUpdateTimestamp = lastUpdateTimestamp; this.lastUpdateTimestamp = lastUpdateTimestamp;
this.givenName = givenName; this.givenName = givenName;
@ -49,7 +46,6 @@ public class Profile {
this.mobileCoinAddress = mobileCoinAddress; this.mobileCoinAddress = mobileCoinAddress;
this.unidentifiedAccessMode = unidentifiedAccessMode; this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities; this.capabilities = capabilities;
this.phoneNumberSharingMode = phoneNumberSharingMode;
} }
private Profile(final Builder builder) { private Profile(final Builder builder) {
@ -62,7 +58,6 @@ public class Profile {
mobileCoinAddress = builder.mobileCoinAddress; mobileCoinAddress = builder.mobileCoinAddress;
unidentifiedAccessMode = builder.unidentifiedAccessMode; unidentifiedAccessMode = builder.unidentifiedAccessMode;
capabilities = builder.capabilities; capabilities = builder.capabilities;
phoneNumberSharingMode = builder.phoneNumberSharingMode;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -141,10 +136,6 @@ public class Profile {
return capabilities; return capabilities;
} }
public PhoneNumberSharingMode getPhoneNumberSharingMode() {
return phoneNumberSharingMode;
}
public enum UnidentifiedAccessMode { public enum UnidentifiedAccessMode {
UNKNOWN, UNKNOWN,
DISABLED, DISABLED,
@ -162,7 +153,9 @@ public class Profile {
public enum Capability { public enum Capability {
storage, storage,
storageServiceEncryptionV2Capability; gv1Migration,
senderKey,
announcementGroup;
public static Capability valueOfOrNull(String value) { public static Capability valueOfOrNull(String value) {
try { try {
@ -210,7 +203,6 @@ public class Profile {
private byte[] mobileCoinAddress; private byte[] mobileCoinAddress;
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
private Set<Capability> capabilities = Collections.emptySet(); private Set<Capability> capabilities = Collections.emptySet();
private PhoneNumberSharingMode phoneNumberSharingMode;
private long lastUpdateTimestamp = 0; private long lastUpdateTimestamp = 0;
private Builder() { private Builder() {
@ -251,11 +243,6 @@ public class Profile {
return this; return this;
} }
public Builder withPhoneNumberSharingMode(final PhoneNumberSharingMode val) {
phoneNumberSharingMode = val;
return this;
}
public Profile build() { public Profile build() {
return new Profile(this); return new Profile(this);
} }

View File

@ -10,19 +10,12 @@ public class ProofRequiredException extends Exception {
private final String token; private final String token;
private final Set<Option> options; private final Set<Option> options;
private final long retryAfterMilliseconds; private final long retryAfterSeconds;
public ProofRequiredException(final String token, final Set<Option> options, final long retryAfterMilliseconds) { public ProofRequiredException(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
super("Rate limit"); this.token = e.getToken();
this.token = token; this.options = e.getOptions().stream().map(Option::from).collect(Collectors.toSet());
this.options = options; this.retryAfterSeconds = e.getRetryAfterSeconds();
this.retryAfterMilliseconds = retryAfterMilliseconds;
}
public static ProofRequiredException from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
return new ProofRequiredException(e.getToken(),
e.getOptions().stream().map(Option::from).collect(Collectors.toSet()),
e.getRetryAfterSeconds() * 1000L);
} }
public String getToken() { public String getToken() {
@ -33,17 +26,17 @@ public class ProofRequiredException extends Exception {
return options; return options;
} }
public long getRetryAfterMilliseconds() { public long getRetryAfterSeconds() {
return retryAfterMilliseconds; return retryAfterSeconds;
} }
public enum Option { public enum Option {
CAPTCHA, RECAPTCHA,
PUSH_CHALLENGE; PUSH_CHALLENGE;
static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) { static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) {
return switch (option) { return switch (option) {
case CAPTCHA -> CAPTCHA; case RECAPTCHA -> RECAPTCHA;
case PUSH_CHALLENGE -> PUSH_CHALLENGE; case PUSH_CHALLENGE -> PUSH_CHALLENGE;
}; };
} }

View File

@ -2,18 +2,14 @@ package org.asamk.signal.manager.api;
public class RateLimitException extends Exception { public class RateLimitException extends Exception {
private final Long retryAfterMilliseconds; private final long nextAttemptTimestamp;
public RateLimitException(final Long retryAfterMilliseconds) { public RateLimitException(final long nextAttemptTimestamp) {
super("Rate limit"); super("Rate limit");
this.retryAfterMilliseconds = retryAfterMilliseconds; this.nextAttemptTimestamp = nextAttemptTimestamp;
} }
public static RateLimitException from(org.whispersystems.signalservice.api.push.exceptions.RateLimitException e) { public long getNextAttemptTimestamp() {
return new RateLimitException(e.getRetryAfterMilliseconds().orElse(null)); return nextAttemptTimestamp;
}
public Long getRetryAfterMilliseconds() {
return retryAfterMilliseconds;
} }
} }

View File

@ -1,9 +1,3 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
public record ReceiveConfig( public record ReceiveConfig(boolean ignoreAttachments, boolean ignoreStories, boolean sendReadReceipts) {}
boolean ignoreAttachments,
boolean ignoreStories,
boolean ignoreAvatars,
boolean ignoreStickers,
boolean sendReadReceipts
) {}

View File

@ -20,16 +20,13 @@ public class Recipient {
private final Profile profile; private final Profile profile;
private final Boolean discoverable;
public Recipient( public Recipient(
final RecipientId recipientId, final RecipientId recipientId,
final RecipientAddress address, final RecipientAddress address,
final Contact contact, final Contact contact,
final ProfileKey profileKey, final ProfileKey profileKey,
final ExpiringProfileKeyCredential expiringProfileKeyCredential, final ExpiringProfileKeyCredential expiringProfileKeyCredential,
final Profile profile, final Profile profile
final Boolean discoverable
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.address = address; this.address = address;
@ -37,7 +34,6 @@ public class Recipient {
this.profileKey = profileKey; this.profileKey = profileKey;
this.expiringProfileKeyCredential = expiringProfileKeyCredential; this.expiringProfileKeyCredential = expiringProfileKeyCredential;
this.profile = profile; this.profile = profile;
this.discoverable = discoverable;
} }
private Recipient(final Builder builder) { private Recipient(final Builder builder) {
@ -45,9 +41,8 @@ public class Recipient {
address = builder.address; address = builder.address;
contact = builder.contact; contact = builder.contact;
profileKey = builder.profileKey; profileKey = builder.profileKey;
expiringProfileKeyCredential = builder.expiringProfileKeyCredential; expiringProfileKeyCredential = builder.expiringProfileKeyCredential1;
profile = builder.profile; profile = builder.profile;
discoverable = builder.discoverable;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -60,7 +55,7 @@ public class Recipient {
builder.address = copy.getAddress(); builder.address = copy.getAddress();
builder.contact = copy.getContact(); builder.contact = copy.getContact();
builder.profileKey = copy.getProfileKey(); builder.profileKey = copy.getProfileKey();
builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential(); builder.expiringProfileKeyCredential1 = copy.getExpiringProfileKeyCredential();
builder.profile = copy.getProfile(); builder.profile = copy.getProfile();
return builder; return builder;
} }
@ -89,10 +84,6 @@ public class Recipient {
return profile; return profile;
} }
public Boolean getDiscoverable() {
return discoverable;
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) return true; if (this == o) return true;
@ -117,9 +108,8 @@ public class Recipient {
private RecipientAddress address; private RecipientAddress address;
private Contact contact; private Contact contact;
private ProfileKey profileKey; private ProfileKey profileKey;
private ExpiringProfileKeyCredential expiringProfileKeyCredential; private ExpiringProfileKeyCredential expiringProfileKeyCredential1;
private Profile profile; private Profile profile;
private Boolean discoverable;
private Builder() { private Builder() {
} }
@ -145,7 +135,7 @@ public class Recipient {
} }
public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) { public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) {
expiringProfileKeyCredential = val; expiringProfileKeyCredential1 = val;
return this; return this;
} }
@ -154,11 +144,6 @@ public class Recipient {
return this; return this;
} }
public Builder withDiscoverable(final Boolean val) {
discoverable = val;
return this;
}
public Recipient build() { public Recipient build() {
return new Recipient(this); return new Recipient(this);
} }

View File

@ -1,55 +1,49 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.signal.core.util.UuidUtil; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
public record RecipientAddress( public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Optional<String> username) {
Optional<String> aci, Optional<String> pni, Optional<String> number, Optional<String> username
) {
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID; public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
/** /**
* Construct a RecipientAddress. * Construct a RecipientAddress.
* *
* @param aci The ACI of the user, if available. * @param uuid The UUID of the user, if available.
* @param pni The PNI of the user, if available.
* @param number The phone number of the user, if available. * @param number The phone number of the user, if available.
*/ */
public RecipientAddress { public RecipientAddress {
if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) { uuid = uuid.isPresent() && uuid.get().equals(UNKNOWN_UUID) ? Optional.empty() : uuid;
throw new AssertionError("Must have either a ACI, PNI, username or E164 number!"); if (uuid.isEmpty() && number.isEmpty() && username.isEmpty()) {
throw new AssertionError("Must have either a UUID or E164 number!");
} }
} }
public RecipientAddress(String e164) { public RecipientAddress(UUID uuid, String e164) {
this(null, null, e164, null); this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.empty());
}
public RecipientAddress(UUID uuid, String e164, String username) {
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.ofNullable(username));
}
public RecipientAddress(SignalServiceAddress address) {
this(Optional.of(address.getServiceId().getRawUuid()), address.getNumber(), Optional.empty());
} }
public RecipientAddress(UUID uuid) { public RecipientAddress(UUID uuid) {
this(uuid.toString(), null, null, null); this(Optional.of(uuid), Optional.empty(), Optional.empty());
}
public RecipientAddress(String aci, String pni, String e164, String username) {
this(Optional.ofNullable(aci),
Optional.ofNullable(pni),
Optional.ofNullable(e164),
Optional.ofNullable(username));
}
public Optional<UUID> uuid() {
return aci.map(UUID::fromString);
} }
public String getIdentifier() { public String getIdentifier() {
if (aci.isPresent()) { if (uuid.isPresent()) {
return aci.get(); return uuid.get().toString();
} else if (number.isPresent()) { } else if (number.isPresent()) {
return number.get(); return number.get();
} else if (pni.isPresent()) {
return pni.get();
} else if (username.isPresent()) { } else if (username.isPresent()) {
return username.get(); return username.get();
} else { } else {
@ -60,16 +54,17 @@ public record RecipientAddress(
public String getLegacyIdentifier() { public String getLegacyIdentifier() {
if (number.isPresent()) { if (number.isPresent()) {
return number.get(); return number.get();
} else if (uuid.isPresent()) {
return uuid.get().toString();
} else if (username.isPresent()) {
return username.get();
} else { } else {
return getIdentifier(); throw new AssertionError("Given the checks in the constructor, this should not be possible.");
} }
} }
public boolean matches(RecipientAddress other) { public boolean matches(RecipientAddress other) {
return (aci.isPresent() && other.aci.isPresent() && aci.get().equals(other.aci.get())) return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get()))
|| (
pni.isPresent() && other.pni.isPresent() && pni.get().equals(other.pni.get())
)
|| (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get())) || (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get()))
|| (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get())); || (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get()));
} }

View File

@ -1,9 +1,9 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.signal.core.util.UuidUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID; import java.util.UUID;
@ -24,18 +24,11 @@ public sealed interface RecipientIdentifier {
sealed interface Single extends RecipientIdentifier { sealed interface Single extends RecipientIdentifier {
static Single fromString(String identifier, String localNumber) throws InvalidNumberException { static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
if (UuidUtil.INSTANCE.isUuid(identifier)) { try {
if (UuidUtil.isUuid(identifier)) {
return new Uuid(UUID.fromString(identifier)); return new Uuid(UUID.fromString(identifier));
} }
if (identifier.startsWith("PNI:")) {
final var pni = identifier.substring(4);
if (!UuidUtil.INSTANCE.isUuid(pni)) {
throw new InvalidNumberException("Invalid PNI");
}
return new Pni(UUID.fromString(pni));
}
if (identifier.startsWith("u:")) { if (identifier.startsWith("u:")) {
return new Username(identifier.substring(2)); return new Username(identifier.substring(2));
} }
@ -46,15 +39,16 @@ public sealed interface RecipientIdentifier {
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber); logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
} }
return new Number(normalizedNumber); return new Number(normalizedNumber);
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
throw new InvalidNumberException(e.getMessage(), e);
}
} }
static Single fromAddress(RecipientAddress address) { static Single fromAddress(RecipientAddress address) {
if (address.number().isPresent()) { if (address.number().isPresent()) {
return new Number(address.number().get()); return new Number(address.number().get());
} else if (address.aci().isPresent()) { } else if (address.uuid().isPresent()) {
return new Uuid(UUID.fromString(address.aci().get())); return new Uuid(address.uuid().get());
} else if (address.pni().isPresent()) {
return new Pni(UUID.fromString(address.pni().get().substring(4)));
} else if (address.username().isPresent()) { } else if (address.username().isPresent()) {
return new Username(address.username().get()); return new Username(address.username().get());
} }
@ -77,19 +71,6 @@ public sealed interface RecipientIdentifier {
} }
} }
record Pni(UUID pni) implements Single {
@Override
public String getIdentifier() {
return "PNI:" + pni.toString();
}
@Override
public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, getIdentifier(), null, null);
}
}
record Number(String number) implements Single { record Number(String number) implements Single {
@Override @Override
@ -99,7 +80,7 @@ public sealed interface RecipientIdentifier {
@Override @Override
public RecipientAddress toPartialRecipientAddress() { public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(number); return new RecipientAddress(null, number);
} }
} }
@ -112,7 +93,7 @@ public sealed interface RecipientIdentifier {
@Override @Override
public RecipientAddress toPartialRecipientAddress() { public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, null, null, username); return new RecipientAddress(null, null, username);
} }
} }

View File

@ -9,13 +9,13 @@ public record SendMessageResult(
boolean isNetworkFailure, boolean isNetworkFailure,
boolean isUnregisteredFailure, boolean isUnregisteredFailure,
boolean isIdentityFailure, boolean isIdentityFailure,
RateLimitException rateLimitException, boolean isRateLimitFailure,
ProofRequiredException proofRequiredFailure, ProofRequiredException proofRequiredFailure,
boolean isInvalidPreKeyFailure boolean isInvalidPreKeyFailure
) { ) {
public static SendMessageResult unregisteredFailure(RecipientAddress address) { public static SendMessageResult unregisteredFailure(RecipientAddress address) {
return new SendMessageResult(address, false, false, true, false, null, null, false); return new SendMessageResult(address, false, false, true, false, false, null, false);
} }
public static SendMessageResult from( public static SendMessageResult from(
@ -23,30 +23,16 @@ public record SendMessageResult(
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver RecipientAddressResolver addressResolver
) { ) {
final var rateLimitFailure = sendMessageResult.getRateLimitFailure();
final var proofRequiredFailure = sendMessageResult.getProofRequiredFailure();
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient( return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
sendMessageResult.getAddress())).toApiRecipientAddress(), sendMessageResult.getAddress())).toApiRecipientAddress(),
sendMessageResult.isSuccess(), sendMessageResult.isSuccess(),
sendMessageResult.isNetworkFailure(), sendMessageResult.isNetworkFailure(),
sendMessageResult.isUnregisteredFailure(), sendMessageResult.isUnregisteredFailure(),
sendMessageResult.getIdentityFailure() != null, sendMessageResult.getIdentityFailure() != null,
rateLimitFailure == null ? null : RateLimitException.from(rateLimitFailure), sendMessageResult.getRateLimitFailure() != null || sendMessageResult.getProofRequiredFailure() != null,
proofRequiredFailure == null ? null : ProofRequiredException.from(proofRequiredFailure), sendMessageResult.getProofRequiredFailure() == null
? null
: new ProofRequiredException(sendMessageResult.getProofRequiredFailure()),
sendMessageResult.isInvalidPreKeyFailure()); sendMessageResult.isInvalidPreKeyFailure());
} }
public boolean isRateLimitFailure() {
return this.rateLimitException != null || this.proofRequiredFailure != null;
}
public Long rateLimitRetryAfterMilliseconds() {
if (proofRequiredFailure != null) {
return proofRequiredFailure.getRetryAfterMilliseconds();
} else if (rateLimitException != null) {
return rateLimitException.getRetryAfterMilliseconds();
} else {
return null;
}
}
} }

View File

@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) { public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
@ -20,25 +19,4 @@ public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<S
.allMatch(identityFailure -> identityFailure) .allMatch(identityFailure -> identityFailure)
&& results.values().stream().mapToInt(List::size).sum() > 0; && results.values().stream().mapToInt(List::size).sum() > 0;
} }
public boolean hasOnlyRateLimitFailure() {
return results.values()
.stream()
.flatMap(res -> res.stream().map(SendMessageResult::isRateLimitFailure))
.allMatch(r -> r) && results.values().stream().mapToInt(List::size).sum() > 0;
}
/**
* Longest rate-limit retry-after window across all rate-limited recipients, in milliseconds.
* Null when no recipient reported one (server omitted Retry-After, or no rate-limit failures).
*/
public Long maxRateLimitRetryAfterMilliseconds() {
return results.values()
.stream()
.flatMap(List::stream)
.map(SendMessageResult::rateLimitRetryAfterMilliseconds)
.filter(Objects::nonNull)
.max(Long::compareTo)
.orElse(null);
}
} }

View File

@ -1,7 +1,5 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.signal.core.util.Hex;
import java.util.Arrays; import java.util.Arrays;
public class StickerPackId { public class StickerPackId {
@ -34,9 +32,4 @@ public class StickerPackId {
public int hashCode() { public int hashCode() {
return Arrays.hashCode(id); return Arrays.hashCode(id);
} }
@Override
public String toString() {
return "StickerPackId{" + Hex.toStringCondensed(id) + '}';
}
} }

View File

@ -1,7 +1,7 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.core.util.Hex; import org.whispersystems.signalservice.internal.util.Hex;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -11,7 +11,10 @@ import java.nio.charset.StandardCharsets;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty; import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public record StickerPackUrl(StickerPackId packId, byte[] packKey) { public final class StickerPackUrl {
private final StickerPackId packId;
private final byte[] packKey;
/** /**
* @throws InvalidStickerPackLinkException If url cannot be parsed. * @throws InvalidStickerPackLinkException If url cannot be parsed.
@ -45,6 +48,11 @@ public record StickerPackUrl(StickerPackId packId, byte[] packKey) {
return new StickerPackUrl(packId, packKey); return new StickerPackUrl(packId, packKey);
} }
public StickerPackUrl(final StickerPackId packId, final byte[] packKey) {
this.packId = packId;
this.packKey = packKey;
}
public URI getUrl() { public URI getUrl() {
try { try {
return new URI("https", return new URI("https",
@ -59,7 +67,15 @@ public record StickerPackUrl(StickerPackId packId, byte[] packKey) {
} }
} }
public static final class InvalidStickerPackLinkException extends Exception { public StickerPackId getPackId() {
return packId;
}
public byte[] getPackKey() {
return packKey;
}
public final static class InvalidStickerPackLinkException extends Exception {
public InvalidStickerPackLinkException(String message) { public InvalidStickerPackLinkException(String message) {
super(message); super(message);

View File

@ -1,6 +1,7 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
public enum TrustLevel { public enum TrustLevel {
UNTRUSTED, UNTRUSTED,
@ -16,6 +17,14 @@ public enum TrustLevel {
return TrustLevel.cachedValues[i]; return TrustLevel.cachedValues[i];
} }
public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) {
return switch (identityState) {
case DEFAULT -> TRUSTED_UNVERIFIED;
case UNVERIFIED -> UNTRUSTED;
case VERIFIED -> TRUSTED_VERIFIED;
};
}
public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) { public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) {
return switch (verifiedState) { return switch (verifiedState) {
case DEFAULT -> TRUSTED_UNVERIFIED; case DEFAULT -> TRUSTED_UNVERIFIED;

View File

@ -1,7 +0,0 @@
package org.asamk.signal.manager.api;
import java.util.List;
public record TurnServer(
String username, String password, List<String> urls
) {}

View File

@ -19,8 +19,6 @@ public class UpdateGroup {
private final String avatarFile; private final String avatarFile;
private final Integer expirationTimer; private final Integer expirationTimer;
private final Boolean isAnnouncementGroup; private final Boolean isAnnouncementGroup;
private final String labelEmoji;
private final String labelString;
private UpdateGroup(final Builder builder) { private UpdateGroup(final Builder builder) {
name = builder.name; name = builder.name;
@ -38,8 +36,6 @@ public class UpdateGroup {
avatarFile = builder.avatarFile; avatarFile = builder.avatarFile;
expirationTimer = builder.expirationTimer; expirationTimer = builder.expirationTimer;
isAnnouncementGroup = builder.isAnnouncementGroup; isAnnouncementGroup = builder.isAnnouncementGroup;
labelEmoji = builder.labelEmoji;
labelString = builder.labelString;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -61,9 +57,7 @@ public class UpdateGroup {
copy.editDetailsPermission, copy.editDetailsPermission,
copy.avatarFile, copy.avatarFile,
copy.expirationTimer, copy.expirationTimer,
copy.isAnnouncementGroup, copy.isAnnouncementGroup);
copy.labelEmoji,
copy.labelString);
} }
public static Builder newBuilder( public static Builder newBuilder(
@ -81,9 +75,7 @@ public class UpdateGroup {
final GroupPermission editDetailsPermission, final GroupPermission editDetailsPermission,
final String avatarFile, final String avatarFile,
final Integer expirationTimer, final Integer expirationTimer,
final Boolean isAnnouncementGroup, final Boolean isAnnouncementGroup
final String labelEmoji,
final String labelString
) { ) {
return new Builder(name, return new Builder(name,
description, description,
@ -99,9 +91,7 @@ public class UpdateGroup {
editDetailsPermission, editDetailsPermission,
avatarFile, avatarFile,
expirationTimer, expirationTimer,
isAnnouncementGroup, isAnnouncementGroup);
labelEmoji,
labelString);
} }
public String getName() { public String getName() {
@ -164,14 +154,6 @@ public class UpdateGroup {
return isAnnouncementGroup; return isAnnouncementGroup;
} }
public String getLabelEmoji() {
return labelEmoji;
}
public String getLabelString() {
return labelString;
}
public static final class Builder { public static final class Builder {
private String name; private String name;
@ -189,8 +171,6 @@ public class UpdateGroup {
private String avatarFile; private String avatarFile;
private Integer expirationTimer; private Integer expirationTimer;
private Boolean isAnnouncementGroup; private Boolean isAnnouncementGroup;
private String labelEmoji;
private String labelString;
private Builder() { private Builder() {
} }
@ -210,9 +190,7 @@ public class UpdateGroup {
final GroupPermission editDetailsPermission, final GroupPermission editDetailsPermission,
final String avatarFile, final String avatarFile,
final Integer expirationTimer, final Integer expirationTimer,
final Boolean isAnnouncementGroup, final Boolean isAnnouncementGroup
final String labelEmoji,
final String labelString
) { ) {
this.name = name; this.name = name;
this.description = description; this.description = description;
@ -229,8 +207,6 @@ public class UpdateGroup {
this.avatarFile = avatarFile; this.avatarFile = avatarFile;
this.expirationTimer = expirationTimer; this.expirationTimer = expirationTimer;
this.isAnnouncementGroup = isAnnouncementGroup; this.isAnnouncementGroup = isAnnouncementGroup;
this.labelEmoji = labelEmoji;
this.labelString = labelString;
} }
public Builder withName(final String val) { public Builder withName(final String val) {
@ -308,16 +284,6 @@ public class UpdateGroup {
return this; return this;
} }
public Builder withLabelEmoji(final String val) {
labelEmoji = val;
return this;
}
public Builder withLabelString(final String val) {
labelString = val;
return this;
}
public UpdateGroup build() { public UpdateGroup build() {
return new UpdateGroup(this); return new UpdateGroup(this);
} }

View File

@ -1,82 +0,0 @@
package org.asamk.signal.manager.api;
import org.signal.core.util.Base64;
import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import java.io.IOException;
import java.util.Arrays;
import java.util.regex.Pattern;
public final class UsernameLinkUrl {
private static final Pattern URL_REGEX = Pattern.compile("(https://)?signal.me/?#eu/([a-zA-Z0-9+\\-_/]+)");
private static final String BASE_URL = "https://signal.me/#eu/";
private final String url;
private final UsernameLinkComponents usernameLinkComponents;
public static UsernameLinkUrl fromUri(String url) throws InvalidUsernameLinkException {
final var matcher = URL_REGEX.matcher(url);
if (!matcher.matches()) {
throw new InvalidUsernameLinkException("Invalid username link");
}
final var path = matcher.group(2);
final byte[] allBytes;
try {
allBytes = Base64.decode(path);
} catch (IOException e) {
throw new InvalidUsernameLinkException("Invalid base64 encoding");
}
if (allBytes.length != 48) {
throw new InvalidUsernameLinkException("Invalid username link");
}
final var entropy = Arrays.copyOfRange(allBytes, 0, 32);
final var serverId = Arrays.copyOfRange(allBytes, 32, allBytes.length);
final var serverIdUuid = UuidUtil.INSTANCE.parseOrNull(serverId);
if (serverIdUuid == null) {
throw new InvalidUsernameLinkException("Invalid serverId");
}
return new UsernameLinkUrl(new UsernameLinkComponents(entropy, serverIdUuid));
}
public UsernameLinkUrl(UsernameLinkComponents usernameLinkComponents) {
this.usernameLinkComponents = usernameLinkComponents;
this.url = createUrl(usernameLinkComponents);
}
private static String createUrl(UsernameLinkComponents usernameLinkComponents) {
final var entropy = usernameLinkComponents.getEntropy();
final var serverId = UuidUtil.toByteArray(usernameLinkComponents.getServerId());
final var combined = new byte[entropy.length + serverId.length];
System.arraycopy(entropy, 0, combined, 0, entropy.length);
System.arraycopy(serverId, 0, combined, entropy.length, serverId.length);
final var base64 = Base64.encodeUrlSafeWithoutPadding(combined);
return BASE_URL + base64;
}
public String getUrl() {
return url;
}
public UsernameLinkComponents getComponents() {
return usernameLinkComponents;
}
public static final class InvalidUsernameLinkException extends Exception {
public InvalidUsernameLinkException(String message) {
super(message);
}
public InvalidUsernameLinkException(Throwable cause) {
super(cause);
}
}
}

View File

@ -1,5 +0,0 @@
package org.asamk.signal.manager.api;
import java.util.UUID;
public record UsernameStatus(String username, UUID uuid, boolean unrestrictedUnidentifiedAccess) {}

View File

@ -1,8 +0,0 @@
package org.asamk.signal.manager.api;
public class VerificationMethodNotAvailableException extends Exception {
public VerificationMethodNotAvailableException() {
super("Invalid verification method");
}
}

View File

@ -0,0 +1,18 @@
package org.asamk.signal.manager.config;
import org.whispersystems.signalservice.api.push.TrustStore;
import java.io.InputStream;
class IasTrustStore implements TrustStore {
@Override
public InputStream getKeyStoreInputStream() {
return IasTrustStore.class.getResourceAsStream("ias.store");
}
@Override
public String getKeyStorePassword() {
return "whisper";
}
}

View File

@ -1,3 +1,26 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
public record KeyBackupConfig(String enclaveName, byte[] serviceId, String mrenclave) {} public class KeyBackupConfig {
private final String enclaveName;
private final byte[] serviceId;
private final String mrenclave;
public KeyBackupConfig(final String enclaveName, final byte[] serviceId, final String mrenclave) {
this.enclaveName = enclaveName;
this.serviceId = serviceId;
this.mrenclave = mrenclave;
}
public String getEnclaveName() {
return enclaveName;
}
public byte[] getServiceId() {
return serviceId;
}
public String getMrenclave() {
return mrenclave;
}
}

View File

@ -1,19 +1,21 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network.Environment; import org.bouncycastle.util.encoders.Hex;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.network.config.HttpProxy; import org.whispersystems.signalservice.api.push.TrustStore;
import org.signal.network.config.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.signal.network.config.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
import org.signal.network.config.SignalProxy; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
import org.signal.network.config.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalProxy;
import org.signal.network.config.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.signal.network.config.SignalStorageUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
import org.signal.network.config.SignalSvr2Url; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
import org.signal.network.config.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
import java.util.Base64; import java.util.Base64;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -21,41 +23,39 @@ import java.util.Optional;
import okhttp3.Dns; import okhttp3.Dns;
import okhttp3.Interceptor; import okhttp3.Interceptor;
import static org.asamk.signal.manager.api.ServiceEnvironment.LIVE;
class LiveConfig { class LiveConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder() private final static String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
.decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6"); private final static String SVR2_MRENCLAVE = "6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094";
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4";
private static final String SVR2_MRENCLAVE_LEGACY = "1240acbd4aa26974184844c8a46b1022d3957ac8a76c1fd8f5b1a15141ee0708";
private static final String URL = "https://chat.signal.org"; private final static String KEY_BACKUP_ENCLAVE_NAME = "e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5";
private static final String CDN_URL = "https://cdn.signal.org"; private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode(
private static final String CDN2_URL = "https://cdn2.signal.org"; "3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89");
private static final String CDN3_URL = "https://cdn3.signal.org"; private final static String KEY_BACKUP_MRENCLAVE = "45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c";
private static final String STORAGE_URL = "https://storage.signal.org"; private final static String FALLBACK_KEY_BACKUP_ENCLAVE_NAME = "0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f";
private static final String SIGNAL_CDSI_URL = "https://cdsi.signal.org"; private final static byte[] FALLBACK_KEY_BACKUP_SERVICE_ID = Hex.decode(
private static final String SIGNAL_SVR2_URL = "https://svr2.signal.org"; "187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39");
private static final TrustStore TRUST_STORE = new WhisperTrustStore(); private final static String FALLBACK_KEY_BACKUP_MRENCLAVE = "ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba";
private static final Optional<Dns> dns = Optional.empty(); private final static String URL = "https://chat.signal.org";
private static final Optional<SignalProxy> proxy = Optional.empty(); private final static String CDN_URL = "https://cdn.signal.org";
private static final Optional<HttpProxy> systemProxy = Optional.empty(); 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 String SIGNAL_CDSI_URL = "https://cdsi.signal.org";
private final static String SIGNAL_SVR2_URL = "https://svr2.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() private final static Optional<Dns> dns = Optional.empty();
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw=="); private final static Optional<SignalProxy> proxy = Optional.empty();
private static final byte[] genericServerPublicParams = Base64.getDecoder()
private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P");
private final static byte[] genericServerPublicParams = Base64.getDecoder()
.decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN"); .decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN");
private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
static SignalServiceConfiguration createDefaultServiceConfiguration( static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors final List<Interceptor> interceptors
) { ) {
@ -63,38 +63,42 @@ class LiveConfig {
Map.of(0, Map.of(0,
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
2, 2,
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
3, new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalCdnUrl[]{new SignalCdnUrl(CDN3_URL, TRUST_STORE)}),
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)}, new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)}, new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
interceptors, interceptors,
dns, dns,
proxy, proxy,
systemProxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams, genericServerPublicParams);
backupServerPublicParams,
false);
} }
static List<ECPublicKey> getUnidentifiedSenderTrustRoots() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return List.of(new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT), return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT2));
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) { static KeyBackupConfig createKeyBackupConfig() {
return new ServiceEnvironmentConfig(LIVE, return new KeyBackupConfig(KEY_BACKUP_ENCLAVE_NAME, KEY_BACKUP_SERVICE_ID, KEY_BACKUP_MRENCLAVE);
LIBSIGNAL_NET_ENV, }
createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoots(), static Collection<KeyBackupConfig> createFallbackKeyBackupConfigs() {
CDSI_MRENCLAVE, return List.of(new KeyBackupConfig(FALLBACK_KEY_BACKUP_ENCLAVE_NAME,
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY)); FALLBACK_KEY_BACKUP_SERVICE_ID,
FALLBACK_KEY_BACKUP_MRENCLAVE));
}
static String getCdsiMrenclave() {
return CDSI_MRENCLAVE;
}
static String getSvr2Mrenclave() {
return SVR2_MRENCLAVE;
} }
private LiveConfig() { private LiveConfig() {

View File

@ -3,47 +3,55 @@ package org.asamk.signal.manager.config;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
import org.signal.libsignal.protocol.util.Medium; import org.signal.libsignal.protocol.util.Medium;
import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.push.TrustStore;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.Interceptor; import okhttp3.Interceptor;
public class ServiceConfig { public class ServiceConfig {
public static final int PREKEY_MINIMUM_COUNT = 10; public final static int PREKEY_MINIMUM_COUNT = 10;
public static final int PREKEY_BATCH_SIZE = 100; public final static int PREKEY_BATCH_SIZE = 100;
public static final int PREKEY_MAXIMUM_ID = Medium.MAX_VALUE; public final static int PREKEY_MAXIMUM_ID = Medium.MAX_VALUE;
public static final long PREKEY_ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30); public final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
public static final long PREKEY_STALE_AGE = TimeUnit.DAYS.toMillis(90); public final static long MAX_ENVELOPE_SIZE = 0;
public static final long SIGNED_PREKEY_ROTATE_AGE = TimeUnit.DAYS.toMillis(2); public final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
public final static boolean AUTOMATIC_NETWORK_RETRY = true;
public final static int GROUP_MAX_SIZE = 1001;
public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; private final static KeyStore iasKeyStore;
public static final long MAX_ENVELOPE_SIZE = 0;
public static final int MAX_INCREMENTAL_MACS_PER_ENVELOPE = 10; static {
public static final int MAX_MESSAGE_SIZE_BYTES = 2000; try {
public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; TrustStore contactTrustStore = new IasTrustStore();
public static final boolean AUTOMATIC_NETWORK_RETRY = true;
public static final int GROUP_MAX_SIZE = 1001; var keyStore = KeyStore.getInstance("BKS");
public static final int MAXIMUM_ONE_OFF_REQUEST_SIZE = 3; keyStore.load(contactTrustStore.getKeyStoreInputStream(),
public static final long UNREGISTERED_LIFESPAN = TimeUnit.DAYS.toMillis(30); contactTrustStore.getKeyStorePassword().toCharArray());
iasKeyStore = keyStore;
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var attachmentBackfill = !isPrimaryDevice; final var giftBadges = !isPrimaryDevice;
final var spqr = true; return new AccountAttributes.Capabilities(false, true, true, true, true, giftBadges, false, false);
final var usernameSyncChangeMessage = !isPrimaryDevice; }
final var optionalPhoneNumber = !isPrimaryDevice;
return new AccountAttributes.Capabilities(true, public static KeyStore getIasKeyStore() {
true, return iasKeyStore;
attachmentBackfill,
spqr,
usernameSyncChangeMessage,
optionalPhoneNumber);
} }
public static ServiceEnvironmentConfig getServiceEnvironmentConfig( public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
ServiceEnvironment serviceEnvironment, ServiceEnvironment serviceEnvironment, String userAgent
String userAgent
) { ) {
final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request() final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
.newBuilder() .newBuilder()
@ -53,8 +61,20 @@ public class ServiceConfig {
final var interceptors = List.of(userAgentInterceptor); final var interceptors = List.of(userAgentInterceptor);
return switch (serviceEnvironment) { return switch (serviceEnvironment) {
case LIVE -> LiveConfig.getServiceEnvironmentConfig(interceptors); case LIVE -> new ServiceEnvironmentConfig(serviceEnvironment,
case STAGING -> StagingConfig.getServiceEnvironmentConfig(interceptors); LiveConfig.createDefaultServiceConfiguration(interceptors),
LiveConfig.getUnidentifiedSenderTrustRoot(),
LiveConfig.createKeyBackupConfig(),
LiveConfig.createFallbackKeyBackupConfigs(),
LiveConfig.getCdsiMrenclave(),
LiveConfig.getSvr2Mrenclave());
case STAGING -> new ServiceEnvironmentConfig(serviceEnvironment,
StagingConfig.createDefaultServiceConfiguration(interceptors),
StagingConfig.getUnidentifiedSenderTrustRoot(),
StagingConfig.createKeyBackupConfig(),
StagingConfig.createFallbackKeyBackupConfigs(),
StagingConfig.getCdsiMrenclave(),
StagingConfig.getSvr2Mrenclave());
}; };
} }
} }

View File

@ -1,17 +1,67 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.network.config.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import java.util.List; import java.util.Collection;
public record ServiceEnvironmentConfig( public class ServiceEnvironmentConfig {
ServiceEnvironment type,
Network.Environment netEnvironment, private final ServiceEnvironment type;
SignalServiceConfiguration signalServiceConfiguration, private final SignalServiceConfiguration signalServiceConfiguration;
List<ECPublicKey> unidentifiedSenderTrustRoots,
String cdsiMrenclave, private final ECPublicKey unidentifiedSenderTrustRoot;
List<String> svr2Mrenclaves
) {} private final KeyBackupConfig keyBackupConfig;
private final Collection<KeyBackupConfig> fallbackKeyBackupConfigs;
private final String cdsiMrenclave;
private final String svr2Mrenclave;
public ServiceEnvironmentConfig(
final ServiceEnvironment type,
final SignalServiceConfiguration signalServiceConfiguration,
final ECPublicKey unidentifiedSenderTrustRoot,
final KeyBackupConfig keyBackupConfig,
final Collection<KeyBackupConfig> fallbackKeyBackupConfigs,
final String cdsiMrenclave,
final String svr2Mrenclave
) {
this.type = type;
this.signalServiceConfiguration = signalServiceConfiguration;
this.unidentifiedSenderTrustRoot = unidentifiedSenderTrustRoot;
this.keyBackupConfig = keyBackupConfig;
this.fallbackKeyBackupConfigs = fallbackKeyBackupConfigs;
this.cdsiMrenclave = cdsiMrenclave;
this.svr2Mrenclave = svr2Mrenclave;
}
public ServiceEnvironment getType() {
return type;
}
public SignalServiceConfiguration getSignalServiceConfiguration() {
return signalServiceConfiguration;
}
public ECPublicKey getUnidentifiedSenderTrustRoot() {
return unidentifiedSenderTrustRoot;
}
public KeyBackupConfig getKeyBackupConfig() {
return keyBackupConfig;
}
public Collection<KeyBackupConfig> getFallbackKeyBackupConfigs() {
return fallbackKeyBackupConfigs;
}
public String getCdsiMrenclave() {
return cdsiMrenclave;
}
public String getSvr2Mrenclave() {
return svr2Mrenclave;
}
}

View File

@ -1,19 +1,21 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network; import org.bouncycastle.util.encoders.Hex;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.network.config.HttpProxy; import org.whispersystems.signalservice.api.push.TrustStore;
import org.signal.network.config.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.signal.network.config.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
import org.signal.network.config.SignalProxy; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
import org.signal.network.config.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalProxy;
import org.signal.network.config.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.signal.network.config.SignalStorageUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
import org.signal.network.config.SignalSvr2Url; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
import org.signal.network.config.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
import java.util.Base64; import java.util.Base64;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -21,41 +23,39 @@ import java.util.Optional;
import okhttp3.Dns; import okhttp3.Dns;
import okhttp3.Interceptor; import okhttp3.Interceptor;
import static org.asamk.signal.manager.api.ServiceEnvironment.STAGING;
class StagingConfig { class StagingConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"); .decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder() private final static String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
.decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm"); private final static String SVR2_MRENCLAVE = "a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95";
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302";
private static final String SVR2_MRENCLAVE_LEGACY = "97f151f6ed078edbbfd72fa9cae694dcc08353f1f5e8d9ccd79a971b10ffc535";
private static final String URL = "https://chat.staging.signal.org"; private final static String KEY_BACKUP_ENCLAVE_NAME = "39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b";
private static final String CDN_URL = "https://cdn-staging.signal.org"; private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode(
private static final String CDN2_URL = "https://cdn2-staging.signal.org"; "ee1d0d972b7ea903615670de43ab1b6e7a825e811c70a29bb5fe0f819e0975fa");
private static final String CDN3_URL = "https://cdn3-staging.signal.org"; private final static String KEY_BACKUP_MRENCLAVE = "45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c";
private static final String STORAGE_URL = "https://storage-staging.signal.org"; private final static String FALLBACK_KEY_BACKUP_ENCLAVE_NAME = "dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99";
private static final String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org"; private final static byte[] FALLBACK_KEY_BACKUP_SERVICE_ID = Hex.decode(
private static final String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org"; "4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a");
private static final TrustStore TRUST_STORE = new WhisperTrustStore(); private final static String FALLBACK_KEY_BACKUP_MRENCLAVE = "ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba";
private static final Optional<Dns> dns = Optional.empty(); private final static String URL = "https://chat.staging.signal.org";
private static final Optional<SignalProxy> proxy = Optional.empty(); private final static String CDN_URL = "https://cdn-staging.signal.org";
private static final Optional<HttpProxy> systemProxy = Optional.empty(); private final static String CDN2_URL = "https://cdn2-staging.signal.org";
private final static String SIGNAL_KEY_BACKUP_URL = "https://api-staging.backup.signal.org";
private final static String STORAGE_URL = "https://storage-staging.signal.org";
private final static String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org";
private final static String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() private final static Optional<Dns> dns = Optional.empty();
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ=="); private final static Optional<SignalProxy> proxy = Optional.empty();
private static final byte[] genericServerPublicParams = Base64.getDecoder()
private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj");
private final static byte[] genericServerPublicParams = Base64.getDecoder()
.decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N"); .decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N");
private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
static SignalServiceConfiguration createDefaultServiceConfiguration( static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors final List<Interceptor> interceptors
) { ) {
@ -63,38 +63,42 @@ class StagingConfig {
Map.of(0, Map.of(0,
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
2, 2,
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
3, new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalCdnUrl[]{new SignalCdnUrl(CDN3_URL, TRUST_STORE)}),
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)}, new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)}, new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
interceptors, interceptors,
dns, dns,
proxy, proxy,
systemProxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams, genericServerPublicParams);
backupServerPublicParams,
false);
} }
static List<ECPublicKey> getUnidentifiedSenderTrustRoots() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return List.of(new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT), return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT2));
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) { static KeyBackupConfig createKeyBackupConfig() {
return new ServiceEnvironmentConfig(STAGING, return new KeyBackupConfig(KEY_BACKUP_ENCLAVE_NAME, KEY_BACKUP_SERVICE_ID, KEY_BACKUP_MRENCLAVE);
LIBSIGNAL_NET_ENV, }
createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoots(), static Collection<KeyBackupConfig> createFallbackKeyBackupConfigs() {
CDSI_MRENCLAVE, return List.of(new KeyBackupConfig(FALLBACK_KEY_BACKUP_ENCLAVE_NAME,
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY)); FALLBACK_KEY_BACKUP_SERVICE_ID,
FALLBACK_KEY_BACKUP_MRENCLAVE));
}
static String getCdsiMrenclave() {
return CDSI_MRENCLAVE;
}
static String getSvr2Mrenclave() {
return SVR2_MRENCLAVE;
} }
private StagingConfig() { private StagingConfig() {

View File

@ -1,6 +1,6 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.signal.network.config.TrustStore; import org.whispersystems.signalservice.api.push.TrustStore;
import java.io.InputStream; import java.io.InputStream;

View File

@ -18,8 +18,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
public class GroupUtils { public class GroupUtils {
public static void setGroupContext( public static void setGroupContext(
final SignalServiceDataMessage.Builder messageBuilder, final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo
final GroupInfo groupInfo
) { ) {
if (groupInfo instanceof GroupInfoV1) { if (groupInfo instanceof GroupInfoV1) {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)

View File

@ -1,6 +1,6 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.signal.core.models.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
public interface AccountFileUpdater { public interface AccountFileUpdater {

View File

@ -3,70 +3,49 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.jobs.SyncStorageJob;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils; import org.asamk.signal.manager.util.NumberVerificationUtils;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.core.util.Base64;
import org.signal.core.util.crypto.DeviceNameCipher;
import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.NoSessionException;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.state.KyberPreKeyRecord; import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.protocol.util.KeyHelper;
import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username; import org.signal.libsignal.usernames.Username;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse; import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity; import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.SyncMessage; import org.whispersystems.util.Base64UrlSafe;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import okio.ByteString;
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty; import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class AccountHelper { public class AccountHelper {
private static final Logger logger = LoggerFactory.getLogger(AccountHelper.class); private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class);
private final Context context; private final Context context;
private final SignalAccount account; private final SignalAccount account;
@ -85,42 +64,6 @@ public class AccountHelper {
} }
public void checkAccountState() throws IOException { public void checkAccountState() throws IOException {
if (account.getAci() == null) {
account.setRegistered(false);
throw new IOException("Account without ACI");
}
try {
updateAccountAttributes();
if (account.getPreviousStorageVersion() < 9) {
context.getPreKeyHelper().forceRefreshPreKeys();
} else {
context.getPreKeyHelper().refreshPreKeysIfNecessary();
}
if (account.getPni() == null) {
checkWhoAmiI();
}
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
throw new IOException("Missing PNI identity key, relinking required");
}
if (account.getPreviousStorageVersion() < 10
&& account.isPrimaryDevice()
&& account.getRegistrationLockPin() != null) {
migrateRegistrationPin();
}
if (account.getUsername() != null && account.getUsernameLink() == null) {
try {
tryToSetUsernameLink(new Username(account.getUsername()));
} catch (BaseUsernameException e) {
logger.debug("Invalid local username");
}
}
} catch (DeprecatedVersionException e) {
logger.debug("Signal-Server returned deprecated version exception", e);
throw e;
} catch (AuthorizationFailedException e) {
account.setRegistered(false);
throw e;
}
if (account.getLastReceiveTimestamp() == 0) { if (account.getLastReceiveTimestamp() == 0) {
logger.info("The Signal protocol expects that incoming messages are regularly received."); logger.info("The Signal protocol expects that incoming messages are regularly received.");
} else { } else {
@ -132,6 +75,27 @@ public class AccountHelper {
days); days);
} }
} }
try {
updateAccountAttributes();
context.getPreKeyHelper().refreshPreKeysIfNecessary();
if (account.getAci() == null || account.getPni() == null) {
checkWhoAmiI();
}
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
context.getSyncHelper().requestSyncPniIdentity();
}
if (account.getPreviousStorageVersion() < 4
&& account.isPrimaryDevice()
&& account.getRegistrationLockPin() != null) {
migrateRegistrationPin();
}
} catch (DeprecatedVersionException e) {
logger.debug("Signal-Server returned deprecated version exception", e);
throw e;
} catch (AuthorizationFailedException e) {
account.setRegistered(false);
throw e;
}
} }
public void checkWhoAmiI() throws IOException { public void checkWhoAmiI() throws IOException {
@ -154,11 +118,11 @@ public class AccountHelper {
account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair()); account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
} }
account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
// TODO check and update remote storage
context.getUnidentifiedAccessHelper().rotateSenderCertificates(); context.getUnidentifiedAccessHelper().rotateSenderCertificates();
dependencies.resetAfterAddressChange(); dependencies.resetAfterAddressChange();
context.getGroupV2Helper().clearAuthCredentialCache(); context.getGroupV2Helper().clearAuthCredentialCache();
context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci()); context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
context.getJobExecutor().enqueueJob(new SyncStorageJob());
} }
public void setPni( public void setPni(
@ -175,290 +139,87 @@ public class AccountHelper {
} }
public void startChangeNumber( public void startChangeNumber(
String newNumber, String newNumber, String captcha, boolean voiceVerification
boolean voiceVerification, ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword()); final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
final var registrationApi = accountManager.getRegistrationApi(); String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
account.getSessionId(newNumber), account.getSessionId(newNumber),
id -> account.setSessionId(newNumber, id), id -> account.setSessionId(newNumber, id),
voiceVerification, voiceVerification,
captcha); captcha);
NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification); NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
} }
public void finishChangeNumber( public void finishChangeNumber(
String newNumber, String newNumber, String verificationCode, String pin
String verificationCode, ) throws IncorrectPinException, PinLockedException, IOException {
String pin // TODO create new PNI identity key
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException { final List<OutgoingPushMessage> deviceMessages = null;
for (var attempts = 0; attempts < 5; attempts++) { final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
try { final Map<String, KyberPreKeyEntity> devicePniLastResortKyberPrekeys = null;
finishChangeNumberInternal(newNumber, verificationCode, pin); final Map<String, Integer> pniRegistrationIds = null;
break; var sessionId = account.getSessionId(account.getNumber());
} catch (MismatchedDevicesException e) {
logger.debug("Change number failed with mismatched devices, retrying.");
try {
dependencies.getMessageSender().handleChangeNumberMismatchDevices(e.getMismatchedDevices());
} catch (UntrustedIdentityException ex) {
throw new AssertionError(ex);
}
}
}
}
private void finishChangeNumberInternal(
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
final var pniIdentity = KeyUtils.generateIdentityKeyPair();
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
final var devicePniLastResortKyberPreKeys = new HashMap<Integer, KyberPreKeyEntity>();
final var pniRegistrationIds = new HashMap<Integer, Integer>();
final var selfDeviceId = account.getDeviceId();
SyncMessage.PniChangeNumber selfChangeNumber = null;
final var deviceIds = new ArrayList<Integer>();
deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
final var aci = account.getAci();
final var accountDataStore = account.getSignalServiceDataStore().aci();
final var subDeviceSessions = accountDataStore.getSubDeviceSessions(aci.toString())
.stream()
.filter(deviceId -> accountDataStore.containsSession(new SignalProtocolAddress(aci.toString(),
deviceId)))
.toList();
deviceIds.addAll(subDeviceSessions);
final var messageSender = dependencies.getMessageSender();
for (final var deviceId : deviceIds) {
// Signed Prekey
final SignedPreKeyRecord signedPreKeyRecord;
try {
signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
pniIdentity.getPrivateKey());
final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
signedPreKeyRecord.getKeyPair().getPublicKey(),
signedPreKeyRecord.getSignature());
devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
} catch (InvalidKeyException e) {
throw new AssertionError("unexpected invalid key", e);
}
// Last-resort kyber prekey
final KyberPreKeyRecord lastResortKyberPreKeyRecord;
try {
lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
pniIdentity.getPrivateKey());
final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
lastResortKyberPreKeyRecord.getSignature());
devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
} catch (InvalidKeyException e) {
throw new AssertionError("unexpected invalid key", e);
}
// Registration Id
var pniRegistrationId = -1;
while (pniRegistrationId < 0 || pniRegistrationIds.containsValue(pniRegistrationId)) {
pniRegistrationId = KeyHelper.generateRegistrationId(false);
}
pniRegistrationIds.put(deviceId, pniRegistrationId);
// Device Message
final var pniChangeNumber = new SyncMessage.PniChangeNumber.Builder().identityKeyPair(ByteString.of(
pniIdentity.serialize()))
.signedPreKey(ByteString.of(signedPreKeyRecord.serialize()))
.lastResortKyberPreKey(ByteString.of(lastResortKyberPreKeyRecord.serialize()))
.registrationId(pniRegistrationId)
.newE164(newNumber)
.build();
if (deviceId == selfDeviceId) {
selfChangeNumber = pniChangeNumber;
} else {
try {
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
pniChangeNumber);
encryptedDeviceMessages.add(message);
} catch (UntrustedIdentityException | IOException | InvalidKeyException | NoSessionException e) {
throw new RuntimeException(e);
}
}
}
final var sessionId = account.getSessionId(newNumber);
if (sessionId == null) {
throw new IOException("No change number verification session active");
}
final var result = NumberVerificationUtils.verifyNumber(sessionId, final var result = NumberVerificationUtils.verifyNumber(sessionId,
verificationCode, verificationCode,
pin, pin,
context.getPinHelper(), context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> { (sessionId1, verificationCode1, registrationLock) -> {
final var registrationApi = dependencies.getRegistrationApi(); final var accountManager = dependencies.getAccountManager();
final var accountApi = dependencies.getAccountApi();
try { try {
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1)); Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1));
} catch (AlreadyVerifiedException e) { } catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number // Already verified so can continue changing number
} }
return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1, return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
sessionId1,
null, null,
newNumber, newNumber,
registrationLock, registrationLock,
pniIdentity.getPublicKey(), account.getPniIdentityKeyPair().getPublicKey(),
encryptedDeviceMessages, deviceMessages,
Utils.mapKeys(devicePniSignedPreKeys, Object::toString), devicePniSignedPreKeys,
Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString), devicePniLastResortKyberPrekeys,
Utils.mapKeys(pniRegistrationIds, Object::toString)))); pniRegistrationIds)));
}); });
// TODO handle response
account.clearSessionId(); updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni()));
final var updatePni = PNI.parseOrThrow(result.first().getPni());
if (updatePni.equals(account.getPni())) {
logger.debug("PNI is unchanged after change number");
return;
}
handlePniChangeNumberMessage(selfChangeNumber, updatePni, false);
}
public boolean handlePniChangeNumberMessage(
final SyncMessage.PniChangeNumber pniChangeNumber,
final PNI updatedPni,
final boolean forcePniPreKeyRotation
) {
if (pniChangeNumber.identityKeyPair != null
&& pniChangeNumber.registrationId != null
&& pniChangeNumber.signedPreKey != null) {
logger.debug("New PNI: {}", updatedPni);
try {
setPni(updatedPni,
new IdentityKeyPair(pniChangeNumber.identityKeyPair.toByteArray()),
pniChangeNumber.newE164,
pniChangeNumber.registrationId,
new SignedPreKeyRecord(pniChangeNumber.signedPreKey.toByteArray()),
pniChangeNumber.lastResortKyberPreKey != null
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
: null);
if (forcePniPreKeyRotation) {
try {
context.getPreKeyHelper().forceRefreshPreKeys(ServiceIdType.PNI);
} catch (IOException e) {
logger.warn("Failed to force refresh PNI pre keys after PNI change sync", e);
}
}
return true;
} catch (Exception e) {
logger.warn("Failed to handle change number message", e);
}
}
return false;
} }
public static final int USERNAME_MIN_LENGTH = 3; public static final int USERNAME_MIN_LENGTH = 3;
public static final int USERNAME_MAX_LENGTH = 32; public static final int USERNAME_MAX_LENGTH = 32;
public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException { public String reserveUsername(String nickname) throws IOException, BaseUsernameException {
final var currentUsername = account.getUsername(); final var currentUsername = account.getUsername();
if (currentUsername != null) { if (currentUsername != null) {
final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.')); final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
if (currentNickname.equals(nickname)) { if (currentNickname.equals(nickname)) {
try {
refreshCurrentUsername(); refreshCurrentUsername();
return; return currentUsername;
} catch (IOException | BaseUsernameException e) {
logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
}
} }
} }
final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH); final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
reserveUsername(candidates); final var candidateHashes = new ArrayList<String>();
}
public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
final var currentUsername = account.getUsername();
if (currentUsername != null) {
if (currentUsername.equals(username)) {
try {
refreshCurrentUsername();
return;
} catch (IOException | BaseUsernameException e) {
logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
}
}
}
final var candidates = List.of(new Username(username));
reserveUsername(candidates);
}
private void reserveUsername(final List<Username> candidates) throws IOException {
final var candidateHashes = new ArrayList<byte[]>();
for (final var candidate : candidates) { for (final var candidate : candidates) {
candidateHashes.add(candidate.getHash()); candidateHashes.add(Base64UrlSafe.encodeBytesWithoutPadding(candidate.getHash()));
} }
final var usernameHash = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes)); final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
final var hashIndex = candidateHashes.stream() final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
.filter(candidateHash -> Arrays.equals(candidateHash, usernameHash))
.findFirst()
.map(candidateHashes::indexOf)
.orElse(-1);
if (hashIndex == -1) { if (hashIndex == -1) {
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes."); logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
throw new IOException("Unexpected username response"); throw new IOException("Unexpected username response");
} }
logger.debug("[reserveUsername] Successfully reserved username."); logger.debug("[reserveUsername] Successfully reserved username.");
final var username = candidates.get(hashIndex); final var username = candidates.get(hashIndex).getUsername();
final var linkComponents = confirmUsernameAndCreateNewLink(username); dependencies.getAccountManager().confirmUsername(username, response);
account.setUsername(username.getUsername()); account.setUsername(username);
account.setUsernameLink(linkComponents);
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
account.getRecipientStore().rotateSelfStorageId();
logger.debug("[confirmUsername] Successfully confirmed username."); logger.debug("[confirmUsername] Successfully confirmed username.");
}
public UsernameLinkComponents createUsernameLink(Username username) throws IOException { return username;
try {
Username.UsernameLink link = username.generateLink();
return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
try {
Username.UsernameLink link = username.generateLink();
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
return new UsernameLinkComponents(link.getEntropy(), serverId);
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
private UsernameLinkComponents reclaimUsernameAndLink(
Username username,
UsernameLinkComponents linkComponents
) throws IOException {
try {
Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
return new UsernameLinkComponents(link.getEntropy(), serverId);
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
} }
public void refreshCurrentUsername() throws IOException, BaseUsernameException { public void refreshCurrentUsername() throws IOException, BaseUsernameException {
@ -470,8 +231,7 @@ public class AccountHelper {
final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI(); final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
final var serverUsernameHash = whoAmIResponse.getUsernameHash(); final var serverUsernameHash = whoAmIResponse.getUsernameHash();
final var hasServerUsername = !isEmpty(serverUsernameHash); final var hasServerUsername = !isEmpty(serverUsernameHash);
final var username = new Username(localUsername); final var localUsernameHash = Base64UrlSafe.encodeBytesWithoutPadding(new Username(localUsername).getHash());
final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash());
if (!hasServerUsername) { if (!hasServerUsername) {
logger.debug("No remote username is set."); logger.debug("No remote username is set.");
@ -483,122 +243,55 @@ public class AccountHelper {
if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) { if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
logger.debug("Attempting to resynchronize username."); logger.debug("Attempting to resynchronize username.");
try { tryReserveConfirmUsername(localUsername, localUsernameHash);
tryReserveConfirmUsername(username);
} catch (NonSuccessfulResponseCodeException e) {
logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
e.getMessage(),
e.getClass().getSimpleName());
account.setUsername(null);
account.setUsernameLink(null);
account.getRecipientStore().rotateSelfStorageId();
throw e;
}
} else { } else {
logger.debug("Username already set, not refreshing."); logger.debug("Username already set, not refreshing.");
} }
} }
private void tryReserveConfirmUsername(final Username username) throws IOException { private void tryReserveConfirmUsername(final String username, String localUsernameHash) throws IOException {
final var usernameLink = account.getUsernameLink(); final var response = dependencies.getAccountManager().reserveUsername(List.of(localUsernameHash));
if (usernameLink == null) {
handleResponseException(dependencies.getAccountApi().reserveUsername(List.of(username.getHash())));
logger.debug("[reserveUsername] Successfully reserved existing username."); logger.debug("[reserveUsername] Successfully reserved existing username.");
final var linkComponents = confirmUsernameAndCreateNewLink(username); dependencies.getAccountManager().confirmUsername(username, response);
account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully confirmed existing username."); logger.debug("[confirmUsername] Successfully confirmed existing username.");
} else {
final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
}
account.getRecipientStore().rotateSelfStorageId();
} }
private void tryToSetUsernameLink(Username username) {
for (var i = 1; i < 4; i++) {
try {
final var linkComponents = createUsernameLink(username);
account.setUsernameLink(linkComponents);
break;
} catch (IOException e) {
logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e);
}
}
}
@SuppressWarnings("unchecked")
public void deleteUsername() throws IOException { public void deleteUsername() throws IOException {
handleResponseException(dependencies.getAccountApi().deleteUsernameHash()); dependencies.getAccountManager().deleteUsername();
account.setUsernameLink(null);
account.setUsername(null); account.setUsername(null);
logger.debug("[deleteUsername] Successfully deleted the username."); logger.debug("[deleteUsername] Successfully deleted the username.");
} }
public void setDeviceName(String deviceName) { public void setDeviceName(String deviceName) {
final var encryptedDeviceName = getEncryptedDeviceName(deviceName); final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
account.setEncryptedDeviceName(Base64.encodeWithoutPadding(encryptedDeviceName)); final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
} account.setEncryptedDeviceName(encryptedDeviceName);
public void setDeviceName(int deviceId, String deviceName) throws IOException {
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
.setDeviceName(encryptedDeviceName, deviceId, cont));
context.getSyncHelper().sendDeviceNameChange(deviceId);
}
private byte[] getEncryptedDeviceName(final String deviceName) {
final var identityKey = account.getAciIdentityKeyPair();
return DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), identityKey);
}
public void refreshDeviceName() throws IOException {
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
.getDevices(cont));
final var deviceId = account.getDeviceId();
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
if (device.isPresent()) {
account.setEncryptedDeviceName(device.get().name);
}
} }
public void updateAccountAttributes() throws IOException { public void updateAccountAttributes() throws IOException {
handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null))); dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
} }
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException { public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
final var linkDeviceApi = dependencies.getLinkDeviceApi(); var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
final LinkedDeviceVerificationCodeResponse verificationCode;
try { try {
verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode()); dependencies.getAccountManager()
} catch (DeviceLimitExceededException e) { .addDevice(deviceLinkInfo.deviceIdentifier(),
throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
}
handleResponseException(dependencies.getLinkDeviceApi()
.linkDevice(account.getNumber(),
account.getAci(),
account.getPni(),
deviceLinkInfo.deviceIdentifier(),
deviceLinkInfo.deviceKey(), deviceLinkInfo.deviceKey(),
account.getAciIdentityKeyPair(), account.getAciIdentityKeyPair(),
account.getPniIdentityKeyPair(), account.getPniIdentityKeyPair(),
account.getProfileKey(), account.getProfileKey(),
account.getOrCreateAccountEntropyPool(), verificationCode);
account.getOrCreatePinMasterKey(), } catch (InvalidKeyException e) {
account.getOrCreateMediaRootBackupKey(), throw new InvalidDeviceLinkException("Invalid device link", e);
verificationCode.getVerificationCode(), }
null,
account.getAuthCredentialSalt()));
account.setMultiDevice(true); account.setMultiDevice(true);
context.getJobExecutor().enqueueJob(new SyncStorageJob());
} }
public void removeLinkedDevices(int deviceId) throws IOException { public void removeLinkedDevices(int deviceId) throws IOException {
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi().removeDevice(deviceId, cont)); dependencies.getAccountManager().removeDevice(deviceId);
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi() var devices = dependencies.getAccountManager().getDevices();
.getDevices(cont));
account.setMultiDevice(devices.size() > 1); account.setMultiDevice(devices.size() > 1);
} }
@ -606,23 +299,19 @@ public class AccountHelper {
var masterKey = account.getOrCreatePinMasterKey(); var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey); context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
} }
public void setRegistrationPin(String pin) throws IOException { public void setRegistrationPin(String pin) throws IOException {
var masterKey = account.getOrCreatePinMasterKey(); var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().setRegistrationLockPin(pin, masterKey); context.getPinHelper().setRegistrationLockPin(pin, masterKey);
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
account.setRegistrationLockPin(pin); account.setRegistrationLockPin(pin);
updateAccountAttributes();
} }
public void removeRegistrationPin() throws IOException { public void removeRegistrationPin() throws IOException {
// Remove KBS Pin // Remove KBS Pin
context.getPinHelper().removeRegistrationLockPin(); context.getPinHelper().removeRegistrationLockPin();
handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
account.setRegistrationLockPin(null); account.setRegistrationLockPin(null);
} }
@ -631,7 +320,7 @@ public class AccountHelper {
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
// If this is the primary device, other users can't send messages to this number anymore. // If this is the primary device, other users can't send messages to this number anymore.
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
handleResponseExceptionSuspend(cont -> dependencies.getAccountApi().clearFcmToken(cont)); dependencies.getAccountManager().setGcmId(Optional.empty());
account.setRegistered(false); account.setRegistered(false);
unregisteredListener.call(); unregisteredListener.call();
@ -645,7 +334,7 @@ public class AccountHelper {
} }
account.setRegistrationLockPin(null); account.setRegistrationLockPin(null);
handleResponseException(dependencies.getAccountApi().deleteAccount()); dependencies.getAccountManager().deleteAccount();
account.setRegistered(false); account.setRegistered(false);
unregisteredListener.call(); unregisteredListener.call();

View File

@ -6,19 +6,14 @@ import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.AttachmentStore; import org.asamk.signal.manager.storage.AttachmentStore;
import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -30,14 +25,12 @@ import java.util.List;
public class AttachmentHelper { public class AttachmentHelper {
private static final Logger logger = LoggerFactory.getLogger(AttachmentHelper.class); private final static Logger logger = LoggerFactory.getLogger(AttachmentHelper.class);
private final SignalDependencies dependencies; private final SignalDependencies dependencies;
private final AttachmentStore attachmentStore; private final AttachmentStore attachmentStore;
private final Context context;
public AttachmentHelper(final Context context) { public AttachmentHelper(final Context context) {
this.context = context;
this.dependencies = context.getDependencies(); this.dependencies = context.getDependencies();
this.attachmentStore = context.getAttachmentStore(); this.attachmentStore = context.getAttachmentStore();
} }
@ -50,87 +43,19 @@ public class AttachmentHelper {
return attachmentStore.retrieveAttachment(id); return attachmentStore.retrieveAttachment(id);
} }
public List<SignalServiceAttachment> uploadAttachments( public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
final List<String> attachments, var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments);
boolean voiceNote
) throws AttachmentInvalidException, IOException {
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
try {
// Upload attachments here, so we only upload once even for multiple recipients // Upload attachments here, so we only upload once even for multiple recipients
final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size()); var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
for (final var attachmentStream : attachmentStreams) { for (var attachmentStream : attachmentStreams) {
attachmentPointers.add(uploadAttachment(attachmentStream)); attachmentPointers.add(uploadAttachment(attachmentStream));
} }
return attachmentPointers; return attachmentPointers;
} finally {
for (final var attachmentStream : attachmentStreams) {
attachmentStream.close();
}
}
}
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
return uploadAttachments(attachments, false);
}
private List<SignalServiceAttachmentStream> createAttachmentStreams(
List<String> attachments,
boolean voiceNote
) throws AttachmentInvalidException, IOException {
if (attachments == null) {
return null;
}
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
for (var attachment : attachments) {
final var attachmentStream = getAttachmentStream(attachment, voiceNote);
signalServiceAttachments.add(attachmentStream);
}
return signalServiceAttachments;
}
private SignalServiceAttachmentStream getAttachmentStream(
final String attachment,
final boolean voiceNote
) throws AttachmentInvalidException {
try {
// Reject local files that point into the signal-cli data directory
if (attachment != null && !attachment.startsWith("data:")) {
try {
final var file = new File(attachment);
final var canonical = file.getCanonicalFile();
final var dataPath = context.getAccount().getDataPath().getCanonicalFile();
if (canonical.toPath().startsWith(dataPath.toPath())) {
throw new AttachmentInvalidException(attachment,
new IOException("Attaching files from the signal-cli data directory is not allowed"));
}
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
final var streamDetailsAndFileName = Utils.createStreamDetails(attachment);
final var streamDetails = streamDetailsAndFileName.first();
final var uploadSpec = getResumableUploadSpec(streamDetails);
return AttachmentUtils.createAttachmentStream(streamDetails,
streamDetailsAndFileName.second(),
voiceNote,
uploadSpec);
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
public ResumableUploadSpec getResumableUploadSpec(final StreamDetails streamDetails) throws IOException {
final var streamLength = streamDetails.getLength();
final var ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(
streamLength));
return dependencies.getCdnService().getResumableUploadSpecBlocking(ciphertextLength);
} }
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException { public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
final var attachmentStream = getAttachmentStream(attachment, false); var attachmentStream = AttachmentUtils.createAttachmentStream(attachment);
return uploadAttachment(attachmentStream); return uploadAttachment(attachmentStream);
} }
@ -166,7 +91,9 @@ public class AttachmentHelper {
retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream)); retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
} }
public void retrieveAttachment(SignalServiceAttachment attachment, AttachmentHandler consumer) throws IOException { public void retrieveAttachment(
SignalServiceAttachment attachment, AttachmentHandler consumer
) throws IOException {
if (attachment.isStream()) { if (attachment.isStream()) {
var input = attachment.asStream().getInputStream(); var input = attachment.asStream().getInputStream();
// don't close input stream here, it might be reused later (e.g. with contact sync messages ...) // don't close input stream here, it might be reused later (e.g. with contact sync messages ...)
@ -191,18 +118,11 @@ public class AttachmentHelper {
} }
private InputStream retrieveAttachmentAsStream( private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, SignalServiceAttachmentPointer pointer, File tmpFile
File tmpFile
) throws IOException { ) throws IOException {
if (pointer.getDigest().isEmpty()) {
throw new IOException("Attachment pointer has no digest.");
}
try { try {
return dependencies.getMessageReceiver() return dependencies.getMessageReceiver()
.retrieveAttachment(pointer, .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
tmpFile,
ServiceConfig.MAX_ATTACHMENT_SIZE,
AttachmentCipherInputStream.IntegrityCheck.forEncryptedDigest(pointer.getDigest().get()));
} catch (MissingConfigurationException | InvalidMessageException e) { } catch (MissingConfigurationException | InvalidMessageException e) {
throw new IOException(e); throw new IOException(e);
} }

View File

@ -1,809 +0,0 @@
package org.asamk.signal.manager.helper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.CallInfo;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.TurnServer;
import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.IdentityKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.asamk.signal.manager.util.Utils.callIdUnsigned;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
/**
* Manages active voice calls: tracks state, spawns/monitors the signal-call-tunnel
* subprocess, routes incoming call messages, and handles timeouts.
*/
public class CallManager implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(CallManager.class);
private static final long RING_TIMEOUT_MS = 60_000;
private static final ObjectMapper mapper = new ObjectMapper();
private final Context context;
private final SignalAccount account;
private final SignalDependencies dependencies;
private final Map<Long, CallState> activeCalls = new ConcurrentHashMap<>();
private final List<Manager.CallEventListener> callEventListeners = new CopyOnWriteArrayList<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
var t = new Thread(r, "call-timeout-scheduler");
t.setDaemon(true);
return t;
});
public CallManager(final Context context) {
this.context = context;
this.account = context.getAccount();
this.dependencies = context.getDependencies();
}
public void addCallEventListener(Manager.CallEventListener listener) {
callEventListeners.add(listener);
}
public void removeCallEventListener(Manager.CallEventListener listener) {
callEventListeners.remove(listener);
}
private void fireCallEvent(CallState state, String reason) {
var callInfo = state.toCallInfo(account.getRecipientAddressResolver());
for (var listener : callEventListeners) {
try {
listener.handleCallEvent(callInfo, reason);
} catch (Throwable e) {
logger.warn("Call event listener failed, ignoring", e);
}
}
}
public CallInfo startOutgoingCall(
final RecipientId recipientId
) throws IOException {
var callId = generateCallId();
var recipientAddress = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
var state = new CallState(callId, CallInfo.State.RINGING_OUTGOING, recipientId, null, true);
logger.debug("Starting outgoing call {} to {} (recipientId: {})",
callIdUnsigned(callId),
recipientAddress,
recipientId);
activeCalls.put(callId, state);
dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
fireCallEvent(state, null);
// Spawn call tunnel binary and connect control channel
spawnMediaTunnel(state);
// Fetch TURN servers
var turnServers = getTurnServers();
// Send createOutgoingCall + proceed via control channel
var createMsg = mapper.createObjectNode();
createMsg.put("type", "createOutgoingCall");
createMsg.put("callId", Utils.callIdUnsigned(callId));
createMsg.put("peerId", recipientAddress.toString());
sendControlMessage(state, writeJson(createMsg));
sendProceed(state, callId, turnServers);
// Schedule ring timeout
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
logger.debug("Started outgoing call {} to {}", callIdUnsigned(callId), recipientAddress);
return state.toCallInfo(account.getRecipientAddressResolver());
}
public CallInfo acceptIncomingCall(final long callId) throws IOException {
final var state = getActiveCall(callId);
if (state.state != CallInfo.State.RINGING_INCOMING) {
throw new IOException("Call "
+ callId
+ " is not in RINGING_INCOMING state (current: "
+ state.state
+ ")");
}
// Defer the accept until the tunnel reports Ringing state.
// Sending accept too early (while RingRTC is in ConnectingBeforeAccepted)
// causes it to be silently dropped.
state.acceptPending = true;
// If the tunnel is already in Ringing state, send immediately
sendAcceptIfReady(state);
state.state = CallInfo.State.CONNECTING;
fireCallEvent(state, null);
logger.debug("Accepted incoming call {}", callIdUnsigned(callId));
return state.toCallInfo(account.getRecipientAddressResolver());
}
public void hangupCall(final long callId) throws IOException {
getActiveCall(callId);
endCall(callId, "local_hangup");
}
public SendMessageResult rejectCall(final long callId) throws IOException {
final var callState = getActiveCall(callId);
final var result = sendBusyMessage(callState.callId, callState.recipientId, callState.deviceId);
endCall(callId, "rejected");
return result;
}
public List<CallInfo> listActiveCalls() {
return activeCalls.values()
.stream()
.map((CallState callState) -> callState.toCallInfo(account.getRecipientAddressResolver()))
.toList();
}
public List<TurnServer> getTurnServers() throws IOException {
try {
var turnServerList = handleResponseException(dependencies.getCallingApi().getTurnServerInfo());
return turnServerList.stream()
.map(info -> new TurnServer(info.getUsername(), info.getPassword(), info.getUrls()))
.toList();
} catch (Throwable e) {
logger.warn("Failed to get TURN server info, returning empty list", e);
return List.of();
}
}
// --- Incoming call message handling ---
public void handleIncomingOffer(
final RecipientId recipientId,
final int deviceId,
final long callId,
final MessageEnvelope.Call.Offer.Type type,
final byte[] opaque
) {
if (callEventListeners.isEmpty()) {
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
callIdUnsigned(callId));
return;
}
var senderAddress = account.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId)
.toApiRecipientAddress();
logger.debug("Incoming offer opaque ({} bytes)", opaque == null ? 0 : opaque.length);
var state = new CallState(callId, CallInfo.State.RINGING_INCOMING, recipientId, deviceId, false);
logger.debug("Starting incoming call {} from {} (recipientId: {})",
callIdUnsigned(callId),
senderAddress,
recipientId);
activeCalls.put(callId, state);
// Spawn call tunnel binary immediately
spawnMediaTunnel(state);
// Get identity keys for the receivedOffer message
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
// Fetch TURN servers
List<TurnServer> turnServers;
try {
turnServers = getTurnServers();
} catch (IOException e) {
logger.warn("Failed to get TURN servers for incoming call {}", callIdUnsigned(callId), e);
turnServers = List.of();
}
// Send receivedOffer to subprocess
var offerMsg = mapper.createObjectNode();
offerMsg.put("type", "receivedOffer");
offerMsg.put("callId", Utils.callIdUnsigned(callId));
offerMsg.put("peerId", senderAddress.toString());
offerMsg.put("senderDeviceId", deviceId);
offerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
offerMsg.put("age", 0);
offerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
offerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
sendControlMessage(state, writeJson(offerMsg));
// Send proceed with TURN servers
sendProceed(state, callId, turnServers);
fireCallEvent(state, null);
// Schedule ring timeout
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
logger.debug("Incoming call {} from {}", callIdUnsigned(callId), senderAddress);
}
public void handleIncomingAnswer(final long callId, final int deviceId, final byte[] opaque) {
var state = activeCalls.get(callId);
if (state == null) {
logger.warn("Received answer for unknown call {}", callIdUnsigned(callId));
return;
}
// Get identity keys
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
// Forward raw opaque to subprocess
var answerMsg = mapper.createObjectNode();
answerMsg.put("type", "receivedAnswer");
answerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
answerMsg.put("senderDeviceId", deviceId);
answerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
answerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
sendControlMessage(state, writeJson(answerMsg));
state.deviceId = deviceId;
state.state = CallInfo.State.CONNECTING;
fireCallEvent(state, null);
logger.debug("Received answer for call {}", callIdUnsigned(callId));
}
public void handleIncomingIceCandidate(final long callId, final byte[] opaque, final int deviceId) {
var state = activeCalls.get(callId);
if (state == null) {
logger.debug("Received ICE candidate for unknown call {}", callIdUnsigned(callId));
return;
}
// Forward to subprocess as receivedIce
var iceMsg = mapper.createObjectNode();
iceMsg.put("type", "receivedIce");
iceMsg.put("senderDeviceId", deviceId);
var candidates = iceMsg.putArray("candidates");
candidates.add(java.util.Base64.getEncoder().encodeToString(opaque));
sendControlMessage(state, writeJson(iceMsg));
logger.debug("Forwarded ICE candidate to tunnel for call {}", callIdUnsigned(callId));
}
public void handleIncomingHangup(final long callId) {
if (callEventListeners.isEmpty() && !activeCalls.containsKey(callId)) {
return;
}
endCall(callId, "remote_hangup");
}
public void handleIncomingBusy(final long callId) {
if (callEventListeners.isEmpty() && !activeCalls.containsKey(callId)) {
return;
}
endCall(callId, "remote_busy");
}
// --- Internal helpers ---
private CallState getActiveCall(final long callId) throws IOException {
var state = activeCalls.get(callId);
if (state == null) {
throw new IOException("No active call with id " + callIdUnsigned(callId));
}
return state;
}
private SendMessageResult sendBusyMessage(final long callId, final RecipientId recipientId, final int deviceId) {
var busyMessage = new BusyMessage(callId);
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, deviceId);
return context.getSendHelper().sendCallMessage(callMessage, recipientId);
}
private void sendControlMessage(CallState state, String json) {
if (state.controlWriter == null) {
logger.debug("Queueing control message for call {} (not yet connected): {}",
callIdUnsigned(state.callId),
json);
state.pendingControlMessages.add(json);
return;
}
state.controlWriter.println(json);
}
private void sendProceed(CallState state, long callId, List<TurnServer> turnServers) {
var proceedMsg = mapper.createObjectNode();
proceedMsg.put("type", "proceed");
proceedMsg.put("callId", Utils.callIdUnsigned(callId));
proceedMsg.put("hideIp", false);
var iceServers = proceedMsg.putArray("iceServers");
for (var ts : turnServers) {
var server = iceServers.addObject();
server.put("username", ts.username());
server.put("password", ts.password());
var urls = server.putArray("urls");
for (var url : ts.urls()) {
urls.add(url);
}
}
sendControlMessage(state, writeJson(proceedMsg));
}
private void spawnMediaTunnel(CallState state) {
try {
var command = new ArrayList<>(List.of(findTunnelBinary()));
var processBuilder = new ProcessBuilder(command);
// Keep stdout and stderr separate: stdout = control protocol, stderr = logging
processBuilder.redirectErrorStream(false);
var process = processBuilder.start();
state.tunnelProcess = process;
// Write config JSON to stdin, then keep stdin open for control messages
var config = buildConfig(state);
var stdinStream = process.getOutputStream();
stdinStream.write(config.getBytes(StandardCharsets.UTF_8));
stdinStream.write('\n');
stdinStream.flush();
// stdin is the control write channel
state.controlWriter = new PrintWriter(new OutputStreamWriter(stdinStream, StandardCharsets.UTF_8), true);
// Flush any pending control messages
for (var msg : state.pendingControlMessages) {
state.controlWriter.println(msg);
}
state.pendingControlMessages.clear();
// If accept was deferred, send it now
sendAcceptIfReady(state);
// Read control events from subprocess stdout
Thread.ofVirtual()
.name("control-read-" + callIdUnsigned(state.callId))
.start(() -> readControlEvents(state, process.getInputStream()));
// Drain subprocess stderr to prevent pipe buffer deadlock
Thread.ofVirtual().name("tunnel-stderr-" + callIdUnsigned(state.callId)).start(() -> {
try (var reader = new BufferedReader(new InputStreamReader(process.getErrorStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
logger.debug("[tunnel-{}] {}", callIdUnsigned(state.callId), line);
}
} catch (IOException ignored) {
}
});
// Monitor process exit
process.onExit().thenAcceptAsync(p -> {
logger.debug("Tunnel for call {} exited with code {}", callIdUnsigned(state.callId), p.exitValue());
if (activeCalls.containsKey(state.callId)) {
endCall(state.callId, "tunnel_exit");
}
});
logger.debug("Spawned signal-call-tunnel for call {}", callIdUnsigned(state.callId));
} catch (Exception e) {
logger.error("Failed to spawn tunnel for call {}", callIdUnsigned(state.callId), e);
endCall(state.callId, "tunnel_spawn_error");
}
}
private String findTunnelBinary() {
// Check environment variable first
var envPath = System.getenv("SIGNAL_CALL_TUNNEL_BIN");
if (envPath != null && !envPath.isEmpty()) {
return envPath;
}
// Check relative to the signal-cli installation directory
try {
var codeSource = CallManager.class.getProtectionDomain().getCodeSource();
if (codeSource != null) {
var jarPath = Path.of(codeSource.getLocation().toURI());
var binPath = tunnelBinaryFromCodeSourcePath(jarPath);
if (Files.isExecutable(binPath)) {
return binPath.toString();
}
}
} catch (Exception e) {
logger.debug("Failed to determine install dir from code source", e);
}
// Fall back to PATH
return "signal-call-tunnel";
}
/**
* Resolves the expected tunnel binary path from a code source path.
* The code source (jar or class dir) is expected to be in {@code <install>/lib/},
* so we go up two levels to reach the install root, then look for
* {@code bin/signal-call-tunnel}.
*/
static Path tunnelBinaryFromCodeSourcePath(Path codeSourcePath) {
var installDir = codeSourcePath.getParent().getParent();
return installDir.resolve("bin").resolve("signal-call-tunnel");
}
private String buildConfig(CallState state) {
var config = mapper.createObjectNode();
config.put("call_id", Utils.callIdUnsigned(state.callId));
config.put("is_outgoing", state.isOutgoing);
config.put("local_device_id", 1);
return writeJson(config);
}
private void readControlEvents(CallState state, java.io.InputStream inputStream) {
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
logger.debug("Control event for call {}: {}", callIdUnsigned(state.callId), line);
try {
var json = mapper.readTree(line);
var type = json.has("type") ? json.get("type").asText() : "";
switch (type) {
case "ready" -> {
if (json.has("inputDeviceName")) {
state.inputDeviceName = json.get("inputDeviceName").asText();
}
if (json.has("outputDeviceName")) {
state.outputDeviceName = json.get("outputDeviceName").asText();
}
logger.debug("Tunnel ready for call {}: input={}, output={}",
callIdUnsigned(state.callId),
state.inputDeviceName,
state.outputDeviceName);
}
case "sendOffer" -> {
var opaqueB64 = json.get("opaque").asText();
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
logSendMessageResult(sendOfferViaSignal(state, opaque));
}
case "sendAnswer" -> {
var opaqueB64 = json.get("opaque").asText();
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
logSendMessageResult(sendAnswerViaSignal(state, opaque));
}
case "sendIce" -> {
var candidatesArr = json.get("candidates");
var opaqueList = new ArrayList<byte[]>();
for (var c : candidatesArr) {
opaqueList.add(java.util.Base64.getDecoder().decode(c.get("opaque").asText()));
}
logSendMessageResult(sendIceViaSignal(state, opaqueList));
}
case "sendHangup" -> {
// RingRTC wants us to send a hangup message via Signal protocol.
// This is NOT a local state change local state is handled by stateChange events.
var hangupType = json.has("hangupType")
? json.get("hangupType").asText("normal")
: "normal";
// Skip multi-device hangup types signal-cli is single-device,
// and sending these to the remote peer causes it to terminate the call.
if (hangupType.contains("onanotherdevice")) {
logger.debug("Ignoring multi-device hangup type: {}", hangupType);
} else {
logSendMessageResult(sendHangupViaSignal(state, hangupType));
}
}
case "sendBusy" -> {
logSendMessageResult(sendBusyViaSignal(state));
}
case "stateChange" -> {
var ringrtcState = json.get("state").asText();
var reason = json.has("reason") ? json.get("reason").asText(null) : null;
handleStateChange(state, ringrtcState, reason);
}
case "error" -> {
var message = json.has("message") ? json.get("message").asText("unknown") : "unknown";
logger.error("Tunnel error for call {}: {}", callIdUnsigned(state.callId), message);
endCall(state.callId, "tunnel_error");
}
default -> {
logger.debug("Unknown control event type '{}' for call {}",
type,
callIdUnsigned(state.callId));
}
}
} catch (Exception e) {
logger.warn("Failed to parse control event JSON for call {}: {}",
callIdUnsigned(state.callId),
e.getMessage());
}
}
} catch (IOException e) {
logger.debug("Control read ended for call {}: {}", callIdUnsigned(state.callId), e.getMessage());
}
}
private void handleStateChange(CallState state, String ringrtcState, String reason) {
if (ringrtcState.startsWith("Incoming")) {
// Don't downgrade if we've already accepted
if (state.state == CallInfo.State.CONNECTING) return;
state.state = CallInfo.State.RINGING_INCOMING;
} else if (ringrtcState.startsWith("Outgoing")) {
state.state = CallInfo.State.RINGING_OUTGOING;
} else if ("Ringing".equals(ringrtcState)) {
// Tunnel is now ready to accept flush deferred accept if pending
state.tunnelRinging = true;
sendAcceptIfReady(state);
return;
} else if ("Connected".equals(ringrtcState)) {
state.state = CallInfo.State.CONNECTED;
} else if ("Connecting".equals(ringrtcState)) {
state.state = CallInfo.State.RECONNECTING;
} else if ("Ended".equals(ringrtcState) || "Rejected".equals(ringrtcState)) {
endCall(state.callId, reason != null ? reason : ringrtcState.toLowerCase());
return;
} else if ("Concluded".equals(ringrtcState)) {
// Cleanup, no-op
return;
}
fireCallEvent(state, reason);
}
public static void logSendMessageResult(SendMessageResult result) {
var identifier = result.getAddress().getIdentifier();
if (result.getProofRequiredFailure() != null) {
final var failure = result.getProofRequiredFailure();
logger.warn(
"CAPTCHA proof required for sending to \"{}\", available options \"{}\" with challenge token \"{}\", or wait \"{}\" seconds.\n",
identifier,
failure.getOptions()
.stream()
.map(ProofRequiredException.Option::toString)
.collect(Collectors.joining(", ")),
failure.getToken(),
failure.getRetryAfterSeconds());
} else if (result.isNetworkFailure()) {
logger.warn("Network failure for \"{}\"", identifier);
} else if (result.getRateLimitFailure() != null) {
logger.warn("Rate limit failure for \"{}\"", identifier);
} else if (result.isUnregisteredFailure()) {
logger.warn("Unregistered user \"{}\"", identifier);
} else if (result.getIdentityFailure() != null) {
logger.warn("Untrusted Identity for \"{}\"", identifier);
}
}
private void sendAcceptIfReady(CallState state) {
if (state.acceptPending && state.tunnelRinging && state.controlWriter != null) {
state.acceptPending = false;
logger.debug("Sending deferred accept for call {}", callIdUnsigned(state.callId));
var acceptMsg = mapper.createObjectNode();
acceptMsg.put("type", "accept");
state.controlWriter.println(writeJson(acceptMsg));
}
}
private SendMessageResult sendOfferViaSignal(CallState state, byte[] opaque) {
var offerMessage = new OfferMessage(state.callId, OfferMessage.Type.AUDIO_CALL, opaque);
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, state.deviceId);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
logger.debug("Sent offer via Signal for call {}", callIdUnsigned(state.callId));
return result;
}
private SendMessageResult sendAnswerViaSignal(CallState state, byte[] opaque) {
var answerMessage = new AnswerMessage(state.callId, opaque);
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, state.deviceId);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
logger.debug("Sent answer via Signal for call {}", callIdUnsigned(state.callId));
return result;
}
private SendMessageResult sendIceViaSignal(CallState state, List<byte[]> opaqueList) {
var iceUpdates = opaqueList.stream().map(opaque -> new IceUpdateMessage(state.callId, opaque)).toList();
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, state.deviceId);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
logger.debug("Sent {} ICE candidates via Signal for call {}", opaqueList.size(), callIdUnsigned(state.callId));
return result;
}
private SendMessageResult sendBusyViaSignal(CallState state) {
var busyMessage = new BusyMessage(state.callId);
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, state.deviceId);
return context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
}
private SendMessageResult sendHangupViaSignal(CallState state, String hangupType) {
var type = switch (hangupType) {
case "accepted", "acceptedonanotherdevice" -> HangupMessage.Type.ACCEPTED;
case "declined", "declinedonanotherdevice" -> HangupMessage.Type.DECLINED;
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
default -> HangupMessage.Type.NORMAL;
};
var hangupMessage = new HangupMessage(state.callId, type, 0);
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
return result;
}
private byte[] getRemoteIdentityKey(CallState state) {
try {
var address = context.getRecipientHelper().resolveSignalServiceAddress(state.recipientId);
var serviceId = address.getServiceId();
var identityInfo = account.getIdentityKeyStore().getIdentityInfo(serviceId);
if (identityInfo != null) {
return getRawIdentityKeyBytes(identityInfo.getIdentityKey());
}
} catch (Exception e) {
logger.warn("Failed to get remote identity key for call {}", callIdUnsigned(state.callId), e);
}
logger.warn("Using local identity key as fallback for remote identity key");
return getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
}
/**
* Strip the 0x05 DJB type prefix from a serialized identity key to get the
* raw 32-byte Curve25519 public key. Signal Android does this via
* WebRtcUtil.getPublicKeyBytes() before passing keys to RingRTC.
*/
private static byte[] getRawIdentityKeyBytes(IdentityKey identityKey) {
var serializedKey = identityKey.serialize();
return getRawIdentityKeyBytes(serializedKey);
}
private static byte[] getRawIdentityKeyBytes(final byte[] serializedKey) {
if (serializedKey.length == 33 && serializedKey[0] == 0x05) {
return java.util.Arrays.copyOfRange(serializedKey, 1, serializedKey.length);
}
return serializedKey;
}
private static String writeJson(ObjectNode node) {
try {
return mapper.writeValueAsString(node);
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
throw new RuntimeException("Failed to serialize JSON", e);
}
}
private void endCall(final long callId, final String reason) {
var state = activeCalls.remove(callId);
dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
if (state == null) return;
state.state = CallInfo.State.ENDED;
fireCallEvent(state, reason);
logger.debug("Call {} ended: {}", callIdUnsigned(callId), reason);
// Send Signal protocol hangup to remote peer (unless they initiated the end)
if (!"remote_hangup".equals(reason)
&& !"rejected".equals(reason)
&& !"remote_busy".equals(reason)
&& !"ringrtc_hangup".equals(reason)) {
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, 0);
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
if (!result.isSuccess()) {
logger.warn("Failed to send hangup to remote for call {}", callIdUnsigned(callId));
logSendMessageResult(result);
}
}
// Send hangup via control channel (stdin) before killing process
if (state.controlWriter != null) {
try {
var hangupMsg = mapper.createObjectNode();
hangupMsg.put("type", "hangup");
state.controlWriter.println(writeJson(hangupMsg));
state.controlWriter.close();
} catch (Exception e) {
logger.debug("Failed to send hangup via control channel", e);
}
}
// Kill tunnel process
if (state.tunnelProcess != null && state.tunnelProcess.isAlive()) {
state.tunnelProcess.destroy();
}
}
private void handleRingTimeout(final long callId) {
var state = activeCalls.get(callId);
if (state == null) return;
if (state.state == CallInfo.State.RINGING_INCOMING || state.state == CallInfo.State.RINGING_OUTGOING) {
logger.debug("Call {} ring timeout", callIdUnsigned(callId));
endCall(callId, "ring_timeout");
}
}
private static long generateCallId() {
return new BigInteger(64, new SecureRandom()).longValue();
}
@Override
public void close() {
scheduler.shutdownNow();
for (var callId : new ArrayList<>(activeCalls.keySet())) {
endCall(callId, "shutdown");
}
synchronized (callEventListeners) {
callEventListeners.clear();
}
}
// --- Internal call state tracking ---
static class CallState {
final long callId;
volatile CallInfo.State state;
final RecipientId recipientId;
volatile Integer deviceId;
final boolean isOutgoing;
volatile String inputDeviceName;
volatile String outputDeviceName;
volatile Process tunnelProcess;
volatile PrintWriter controlWriter;
// Control messages queued before the tunnel process starts
final List<String> pendingControlMessages = Collections.synchronizedList(new ArrayList<>());
// Accept deferred until tunnel reports Ringing state
volatile boolean acceptPending = false;
// True once the tunnel has reported "Ringing" (ready to accept)
volatile boolean tunnelRinging = false;
CallState(
long callId,
CallInfo.State state,
RecipientId recipientId,
final Integer deviceId,
boolean isOutgoing
) {
this.callId = callId;
this.state = state;
this.recipientId = recipientId;
this.deviceId = deviceId;
this.isOutgoing = isOutgoing;
}
CallInfo toCallInfo(RecipientAddressResolver addressResolver) {
return new CallInfo(callId,
state,
addressResolver.resolveRecipientAddress(recipientId).toApiRecipientAddress(),
inputDeviceName,
outputDeviceName,
isOutgoing);
}
}
}

Some files were not shown because too many files have changed in this diff Show More