mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-09-01 06:28:11 +00:00
Compare commits
No commits in common. "master" and "v0.11.10" have entirely different histories.
57
.github/workflows/build.yml
vendored
57
.github/workflows/build.yml
vendored
@ -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
58
.github/workflows/ci.yml
vendored
Normal 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', '20' ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
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
|
||||
14
.github/workflows/codeql-analysis.yml
vendored
14
.github/workflows/codeql-analysis.yml
vendored
@ -21,13 +21,13 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Setup Java JDK
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 25
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- 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
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
27
.github/workflows/dependency-graph.yml
vendored
27
.github/workflows/dependency-graph.yml
vendored
@ -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
|
||||
239
.github/workflows/release.yml
vendored
239
.github/workflows/release.yml
vendored
@ -5,78 +5,185 @@ on:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
IMAGE_NAME: signal-cli
|
||||
IMAGE_REGISTRY: ghcr.io/asamk
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
REGISTRY_PASSWORD: ${{ github.token }}
|
||||
ARCHIVE_JAVA_VERSION: 25
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
|
||||
release:
|
||||
needs: build
|
||||
ci_wf:
|
||||
uses: AsamK/signal-cli/.github/workflows/ci.yml@master
|
||||
# ${{ github.repository }} not accepted here
|
||||
|
||||
lib_to_jar:
|
||||
needs: ci_wf
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
signal_cli_version: ${{ steps.cli_ver.outputs.version }}
|
||||
release_id: ${{ steps.create_release.outputs.id }}
|
||||
|
||||
steps:
|
||||
|
||||
- name: Download signal-cli build from CI workflow
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Get signal-cli version
|
||||
id: version
|
||||
id: cli_ver
|
||||
run: |
|
||||
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
||||
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
ver="${GITHUB_REF_NAME#v}"
|
||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create draft release and upload assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract archive
|
||||
run: |
|
||||
gh release create "v${{ steps.version.outputs.version }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--title "v${{ steps.version.outputs.version }}" \
|
||||
--draft \
|
||||
--verify-tag \
|
||||
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
|
||||
"signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \
|
||||
"signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \
|
||||
"signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz"
|
||||
tree .
|
||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
||||
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
|
||||
rm -rf signal-cli-archive-*/
|
||||
|
||||
- name: Get signal-client jar version
|
||||
id: lib_ver
|
||||
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:
|
||||
needs: release
|
||||
needs: ci_wf
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
- 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
|
||||
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/
|
||||
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
|
||||
mv ./signal-cli-*/ build/install/signal-cli
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
uses: redhat-actions/buildah-build@v3
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: ${{ env.IMAGE_NAME }}
|
||||
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
||||
containerfiles: ./Containerfile
|
||||
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
|
||||
containerfiles:
|
||||
./Containerfile
|
||||
oci: true
|
||||
|
||||
- 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 }}
|
||||
@ -90,75 +197,41 @@ jobs:
|
||||
echo "${{ toJSON(steps.push.outputs) }}"
|
||||
|
||||
build-container-native:
|
||||
needs: release
|
||||
needs: ci_wf
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
- 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
|
||||
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/
|
||||
mv signal-cli build/native/nativeCompile/
|
||||
chmod +x build/native/nativeCompile/signal-cli
|
||||
chmod +x ./signal-cli-native/signal-cli
|
||||
mv ./signal-cli-native/signal-cli build/native/nativeCompile/
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
uses: redhat-actions/buildah-build@v3
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: ${{ env.IMAGE_NAME }}
|
||||
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
||||
containerfiles: ./native.Containerfile
|
||||
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
|
||||
containerfiles:
|
||||
./native.Containerfile
|
||||
oci: true
|
||||
|
||||
- name: Push To GHCR
|
||||
uses: redhat-actions/push-to-registry@v3
|
||||
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
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
id: push
|
||||
with:
|
||||
image: ${{ steps.build_image.outputs.image }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
.gradle/
|
||||
.kotlin/
|
||||
.idea/*
|
||||
!.idea/codeStyles/
|
||||
build/
|
||||
@ -14,10 +13,3 @@ out/
|
||||
.DS_Store
|
||||
/bin/
|
||||
/test-config/
|
||||
/dist/
|
||||
/github/
|
||||
man/*.1
|
||||
man/*.5
|
||||
man/man1
|
||||
man/man5
|
||||
.superpowers/
|
||||
|
||||
4
.idea/codeStyles/Project.xml
generated
4
.idea/codeStyles/Project.xml
generated
@ -5,8 +5,8 @@
|
||||
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
||||
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
|
||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="NAMES_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="50" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
|
||||
792
CHANGELOG.md
792
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
||||
@ -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.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
|
||||
|
||||
109
README.md
109
README.md
@ -3,18 +3,13 @@
|
||||
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
||||
It supports registering, verifying, sending and receiving messages.
|
||||
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.
|
||||
|
||||
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
||||
For this use-case, it has a daemon mode with JSON-RPC interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.5.adoc))
|
||||
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)).
|
||||
signal-cli is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a daemon mode with D-BUS
|
||||
interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) and JSON-RPC interface ([documentation](https://github.com/AsamK/signal-cli/wiki/JSON-RPC-service)).
|
||||
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
|
||||
|
||||
You can [build signal-cli](#building) yourself or use
|
||||
@ -23,36 +18,28 @@ Windows. There's also a [docker image and some Linux packages](https://github.co
|
||||
|
||||
System requirements:
|
||||
|
||||
- at least Java Runtime Environment (JRE) 25
|
||||
- at least Java Runtime Environment (JRE) 17
|
||||
- native library: libsignal-client
|
||||
|
||||
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
|
||||
systems/architectures
|
||||
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).
|
||||
|
||||
```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}".tar.gz
|
||||
sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt
|
||||
export VERSION=<latest version, format "x.y.z">
|
||||
wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux.tar.gz
|
||||
sudo tar xf signal-cli-"${VERSION}"-Linux.tar.gz -C /opt
|
||||
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:
|
||||
|
||||
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
|
||||
- [DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service)
|
||||
|
||||
## Usage
|
||||
|
||||
@ -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
|
||||
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)
|
||||
|
||||
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:
|
||||
* Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`)
|
||||
* 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
|
||||
```
|
||||
You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly
|
||||
to the voice call verification by adding the `--voice` switch at the end of above register command.
|
||||
|
||||
Registering may require solving a CAPTCHA
|
||||
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
|
||||
@ -96,27 +64,19 @@ of all country codes.)
|
||||
* Verify the number using the code received via SMS or voice, optionally add `--pin PIN_CODE` if you've added a pin code
|
||||
to your account
|
||||
|
||||
signal-cli -a ACCOUNT verify CODE
|
||||
signal-cli -a ACCOUNT verify CODE
|
||||
|
||||
* Send a message
|
||||
|
||||
```sh
|
||||
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
|
||||
```
|
||||
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
|
||||
|
||||
* 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
|
||||
|
||||
* Receive messages
|
||||
|
||||
signal-cli -a ACCOUNT receive
|
||||
signal-cli -a ACCOUNT receive
|
||||
|
||||
**Hint**: The Signal protocol expects that incoming messages are regularly received (using `daemon` or `receive`
|
||||
command). This is required for the encryption to work efficiently and for getting updates to groups, expiration timer
|
||||
@ -126,8 +86,8 @@ and other features.
|
||||
|
||||
The password and cryptographic keys are created when registering and stored in the current users home directory:
|
||||
|
||||
$XDG_DATA_HOME/signal-cli/data/
|
||||
$HOME/.local/share/signal-cli/data/
|
||||
$XDG_DATA_HOME/signal-cli/data/
|
||||
$HOME/.local/share/signal-cli/data/
|
||||
|
||||
## Building
|
||||
|
||||
@ -136,55 +96,44 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
|
||||
|
||||
1. Checkout the source somewhere on your filesystem with
|
||||
|
||||
git clone https://github.com/AsamK/signal-cli.git
|
||||
git clone https://github.com/AsamK/signal-cli.git
|
||||
|
||||
2. Execute Gradle:
|
||||
|
||||
./gradlew build
|
||||
./gradlew build
|
||||
|
||||
2a. Create shell wrapper in *build/install/signal-cli/bin*:
|
||||
|
||||
./gradlew installDist
|
||||
./gradlew installDist
|
||||
|
||||
2b. Create tar file in *build/distributions*:
|
||||
|
||||
./gradlew distTar
|
||||
./gradlew distTar
|
||||
|
||||
2c. Create a fat tar file in *build/libs/signal-cli-fat*:
|
||||
|
||||
./gradlew fatJar
|
||||
./gradlew fatJar
|
||||
|
||||
2d. Compile and run signal-cli:
|
||||
|
||||
```sh
|
||||
./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.
|
||||
./gradlew run --args="--help"
|
||||
|
||||
### 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
|
||||
work in all situations.
|
||||
|
||||
1. [Install GraalVM and setup the environment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
|
||||
2. Execute Gradle:
|
||||
1. [Install GraalVM and setup the enviroment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
|
||||
2. [Install prerequisites](https://www.graalvm.org/reference-manual/native-image/#prerequisites)
|
||||
3. Execute Gradle:
|
||||
|
||||
./gradlew nativeCompile
|
||||
./gradlew nativeCompile
|
||||
|
||||
The binary is available at *build/native/nativeCompile/signal-cli*
|
||||
|
||||
## FAQ and Troubleshooting
|
||||
|
||||
For frequently asked questions and issues have a look at the [wiki](https://github.com/AsamK/signal-cli/wiki/FAQ).
|
||||
For frequently asked questions and issues have a look at the [wiki](https://github.com/AsamK/signal-cli/wiki/FAQ)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
136
build.gradle.kts
136
build.gradle.kts
@ -1,46 +1,31 @@
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
plugins {
|
||||
java
|
||||
application
|
||||
eclipse
|
||||
`check-lib-versions`
|
||||
id("org.graalvm.buildtools.native") version "1.1.9"
|
||||
id("org.graalvm.buildtools.native") version "0.9.21"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "org.asamk"
|
||||
version = "0.14.8"
|
||||
}
|
||||
version = "0.11.10"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_25
|
||||
targetCompatibility = JavaVersion.VERSION_25
|
||||
|
||||
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
|
||||
}
|
||||
}
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("org.asamk.signal.Main")
|
||||
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
graalvmNative {
|
||||
binaries {
|
||||
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()
|
||||
configurationFileDirectories.from(file("graalvm-config-dir"))
|
||||
if (System.getenv("GRAALVM_HOME") == null) {
|
||||
toolchainDetection.set(true)
|
||||
javaLauncher.set(javaToolchains.launcherFor {
|
||||
languageVersion.set(JavaLanguageVersion.of(25))
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
})
|
||||
} else {
|
||||
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 {
|
||||
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.jackson.databind)
|
||||
implementation(libs.argparse4j)
|
||||
@ -98,20 +42,7 @@ dependencies {
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.slf4j.jul)
|
||||
implementation(libs.logback)
|
||||
implementation(libs.zxing)
|
||||
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()
|
||||
implementation(project(":lib"))
|
||||
}
|
||||
|
||||
configurations {
|
||||
@ -135,68 +66,23 @@ tasks.withType<Jar> {
|
||||
attributes(
|
||||
"Implementation-Title" to project.name,
|
||||
"Implementation-Version" to project.version,
|
||||
"Main-Class" to application.mainClass.get(),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
||||
"Main-Class" to application.mainClass.get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("fatJar", type = Jar::class) {
|
||||
task("fatJar", type = Jar::class) {
|
||||
archiveBaseName.set("${project.name}-fat")
|
||||
exclude(
|
||||
"META-INF/*.SF",
|
||||
"META-INF/**/*.MF",
|
||||
"META-INF/*.DSA",
|
||||
"META-INF/*.RSA",
|
||||
"META-INF/NOTICE*",
|
||||
"META-INF/LICENSE*",
|
||||
"META-INF/INDEX.LIST",
|
||||
"**/module-info.class",
|
||||
"**/module-info.class"
|
||||
)
|
||||
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())
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,11 +7,11 @@ plugins {
|
||||
}
|
||||
|
||||
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
|
||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_25)
|
||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
|
||||
java {
|
||||
targetCompatibility = JavaVersion.VERSION_25
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import groovy.util.XmlSlurper
|
||||
import groovy.util.slurpersupport.GPathResult
|
||||
import org.codehaus.groovy.runtime.ResourceGroovyMethods
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.Dependency
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class CheckLibVersionsPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.tasks.register("checkLibVersions") {
|
||||
project.task("checkLibVersions") {
|
||||
description =
|
||||
"Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded."
|
||||
doLast {
|
||||
@ -24,15 +26,15 @@ class CheckLibVersionsPlugin : Plugin<Project> {
|
||||
val name = dependency.name
|
||||
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
|
||||
try {
|
||||
val dbf = DocumentBuilderFactory.newInstance()
|
||||
val db = dbf.newDocumentBuilder()
|
||||
val doc = db.parse(metaDataUrl)
|
||||
val newest = doc.getElementsByTagName("latest").item(0).textContent
|
||||
val url = ResourceGroovyMethods.toURL(metaDataUrl)
|
||||
val metaDataText = ResourceGroovyMethods.getText(url)
|
||||
val metadata = XmlSlurper().parseText(metaDataText)
|
||||
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest")
|
||||
if (version != newest.toString()) {
|
||||
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.debug("Unable to download or parse {}: {}", metaDataUrl, e.message)
|
||||
logger.debug("Unable to download or parse $metaDataUrl: $e.message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
1747
client/Cargo.lock
generated
1747
client/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,25 @@
|
||||
[package]
|
||||
name = "signal-cli-client"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
|
||||
jsonrpc-core = "18"
|
||||
jsonrpc-core-client = "18"
|
||||
jsonrpc-client-transports = { version = "18", default-features = false, features = [
|
||||
"ipc",
|
||||
] }
|
||||
jsonrpc-derive = "18"
|
||||
jsonrpc-server-utils = "18"
|
||||
log = "0.4"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
|
||||
jsonrpsee = { version = "0.26", features = [
|
||||
"macros",
|
||||
"async-client",
|
||||
"http-client",
|
||||
] }
|
||||
bytes = "1"
|
||||
tokio-util = "0.7"
|
||||
futures-util = "0.3"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["rt", "macros", "net"] }
|
||||
|
||||
[patch.crates-io]
|
||||
jsonrpc-client-transports = { git = "https://github.com/AsamK/jsonrpc", branch = "client_subscribe_named_params" }
|
||||
jsonrpc-derive = { git = "https://github.com/AsamK/jsonrpc", branch = "client_subscribe_named_params" }
|
||||
|
||||
@ -1,30 +1,24 @@
|
||||
use std::{ffi::OsString, net::SocketAddr};
|
||||
|
||||
use clap::{crate_version, Parser, Subcommand, ValueEnum};
|
||||
use std::{ffi::OsString, net::SocketAddr};
|
||||
|
||||
/// JSON-RPC client for signal-cli
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(rename_all = "kebab-case", version = crate_version!())]
|
||||
#[command(rename_all = "kebab-case", version=crate_version!())]
|
||||
pub struct Cli {
|
||||
/// Account to use (for daemon in multi-account mode)
|
||||
#[arg(short = 'a', long)]
|
||||
pub account: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
pub output: Option<String>,
|
||||
|
||||
/// TCP host and port of signal-cli daemon
|
||||
#[arg(long, conflicts_with = "json_rpc_http")]
|
||||
#[arg(long)]
|
||||
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
||||
|
||||
/// UNIX socket address and port of signal-cli daemon
|
||||
#[cfg(unix)]
|
||||
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
||||
#[arg(long)]
|
||||
pub json_rpc_socket: Option<Option<OsString>>,
|
||||
|
||||
/// HTTP URL of signal-cli daemon
|
||||
#[arg(long, conflicts_with = "json_rpc_socket")]
|
||||
pub json_rpc_http: Option<Option<String>>,
|
||||
#[arg(value_enum, long, default_value_t = OutputTypes::Json)]
|
||||
pub output: OutputTypes,
|
||||
|
||||
#[arg(long)]
|
||||
pub verbose: bool,
|
||||
@ -33,18 +27,21 @@ pub struct Cli {
|
||||
pub command: CliCommands,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
#[value(rename_all = "kebab-case")]
|
||||
pub enum OutputTypes {
|
||||
PlainText,
|
||||
Json,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(rename_all = "camelCase", version = crate_version!())]
|
||||
#[command(rename_all = "camelCase", version=crate_version!())]
|
||||
pub enum CliCommands {
|
||||
AddDevice {
|
||||
#[arg(long)]
|
||||
uri: String,
|
||||
},
|
||||
AddStickerPack {
|
||||
#[arg(long)]
|
||||
uri: String,
|
||||
},
|
||||
#[command(rename_all = "kebab-case")]
|
||||
Block {
|
||||
recipient: Vec<String>,
|
||||
@ -56,40 +53,8 @@ pub enum CliCommands {
|
||||
#[arg(long = "ignore-registered")]
|
||||
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 {
|
||||
recipient: Vec<String>,
|
||||
#[arg(long)]
|
||||
username: Vec<String>,
|
||||
},
|
||||
JoinGroup {
|
||||
#[arg(long)]
|
||||
@ -97,7 +62,7 @@ pub enum CliCommands {
|
||||
},
|
||||
Link {
|
||||
#[arg(short = 'n', long)]
|
||||
name: Option<String>,
|
||||
name: String,
|
||||
},
|
||||
ListAccounts,
|
||||
ListContacts {
|
||||
@ -108,10 +73,6 @@ pub enum CliCommands {
|
||||
blocked: Option<bool>,
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
#[arg(long)]
|
||||
detailed: bool,
|
||||
#[arg(long)]
|
||||
internal: bool,
|
||||
},
|
||||
ListDevices,
|
||||
ListGroups {
|
||||
@ -142,15 +103,11 @@ pub enum CliCommands {
|
||||
voice: bool,
|
||||
#[arg(long)]
|
||||
captcha: Option<String>,
|
||||
#[arg(long)]
|
||||
reregister: bool,
|
||||
},
|
||||
RemoveContact {
|
||||
recipient: String,
|
||||
#[arg(long)]
|
||||
forget: bool,
|
||||
#[arg(long)]
|
||||
hide: bool,
|
||||
},
|
||||
RemoveDevice {
|
||||
#[arg(short = 'd', long = "device-id")]
|
||||
@ -176,36 +133,21 @@ pub enum CliCommands {
|
||||
#[arg(short = 'g', long)]
|
||||
group_id: Vec<String>,
|
||||
|
||||
#[arg(short = 'u', long = "username")]
|
||||
username: Vec<String>,
|
||||
|
||||
#[arg(long)]
|
||||
note_to_self: bool,
|
||||
|
||||
#[arg(long)]
|
||||
notify_self: bool,
|
||||
|
||||
#[arg(short = 'e', long)]
|
||||
end_session: bool,
|
||||
|
||||
#[arg(short = 'm', long)]
|
||||
message: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
message_from_stdin: bool,
|
||||
|
||||
#[arg(short = 'a', long)]
|
||||
attachment: Vec<String>,
|
||||
|
||||
#[arg(long)]
|
||||
view_once: bool,
|
||||
|
||||
#[arg(long)]
|
||||
mention: Vec<String>,
|
||||
|
||||
#[arg(long)]
|
||||
text_style: Vec<String>,
|
||||
|
||||
#[arg(long)]
|
||||
quote_timestamp: Option<u64>,
|
||||
|
||||
@ -218,24 +160,6 @@ pub enum CliCommands {
|
||||
#[arg(long)]
|
||||
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)]
|
||||
sticker: Option<String>,
|
||||
|
||||
@ -244,28 +168,6 @@ pub enum CliCommands {
|
||||
|
||||
#[arg(long)]
|
||||
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,
|
||||
SendPaymentNotification {
|
||||
@ -277,117 +179,15 @@ pub enum CliCommands {
|
||||
#[arg(long)]
|
||||
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 {
|
||||
recipient: Vec<String>,
|
||||
|
||||
#[arg(short = 'g', long = "group-id")]
|
||||
group_id: Vec<String>,
|
||||
|
||||
#[arg(short = 'u', long = "username")]
|
||||
username: Vec<String>,
|
||||
|
||||
#[arg(long = "note-to-self")]
|
||||
note_to_self: bool,
|
||||
|
||||
#[arg(long)]
|
||||
notify_self: bool,
|
||||
|
||||
#[arg(short = 'e', long)]
|
||||
emoji: String,
|
||||
|
||||
@ -406,9 +206,6 @@ pub enum CliCommands {
|
||||
SendReceipt {
|
||||
recipient: String,
|
||||
|
||||
#[arg(short = 'u', long = "username")]
|
||||
username: Vec<String>,
|
||||
|
||||
#[arg(short = 't', long = "target-timestamp")]
|
||||
target_timestamp: Vec<u64>,
|
||||
|
||||
@ -425,49 +222,9 @@ pub enum CliCommands {
|
||||
#[arg(short = 's', long)]
|
||||
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 {
|
||||
pin: String,
|
||||
},
|
||||
StartChangeNumber {
|
||||
number: String,
|
||||
#[arg(short = 'v', long)]
|
||||
voice: bool,
|
||||
#[arg(long)]
|
||||
captcha: Option<String>,
|
||||
},
|
||||
SubmitRateLimitChallenge {
|
||||
challenge: String,
|
||||
captcha: String,
|
||||
@ -495,16 +252,6 @@ pub enum CliCommands {
|
||||
UpdateAccount {
|
||||
#[arg(short = 'n', long = "device-name")]
|
||||
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 {
|
||||
#[arg(long = "read-receipts")]
|
||||
@ -527,28 +274,6 @@ pub enum CliCommands {
|
||||
|
||||
#[arg(short = 'n', long)]
|
||||
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 {
|
||||
#[arg(short = 'g', long = "group-id")]
|
||||
@ -598,12 +323,6 @@ pub enum CliCommands {
|
||||
|
||||
#[arg(short = 'e', long)]
|
||||
expiration: Option<u32>,
|
||||
|
||||
#[arg(long = "member-label-emoji")]
|
||||
member_label_emoji: Option<String>,
|
||||
|
||||
#[arg(long = "member-label")]
|
||||
member_label: Option<String>,
|
||||
},
|
||||
UpdateProfile {
|
||||
#[arg(long = "given-name")]
|
||||
@ -618,7 +337,7 @@ pub enum CliCommands {
|
||||
#[arg(long = "about-emoji")]
|
||||
about_emoji: Option<String>,
|
||||
|
||||
#[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")]
|
||||
#[arg(long = "mobile-coin-address")]
|
||||
mobile_coin_address: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
@ -660,10 +379,3 @@ pub enum GroupPermission {
|
||||
EveryMember,
|
||||
OnlyAdmins,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
#[value(rename_all = "kebab-case")]
|
||||
pub enum MessageRequestResponseType {
|
||||
Accept,
|
||||
Delete,
|
||||
}
|
||||
|
||||
@ -1,102 +1,49 @@
|
||||
use std::path::Path;
|
||||
|
||||
use jsonrpsee::async_client::ClientBuilder;
|
||||
use jsonrpsee::core::client::{Error, SubscriptionClientT};
|
||||
use jsonrpsee::http_client::HttpClientBuilder;
|
||||
use jsonrpsee::proc_macros::rpc;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use jsonrpc_client_transports::{transports::ipc, RpcError};
|
||||
use jsonrpc_core::serde::Deserialize;
|
||||
use jsonrpc_derive::rpc;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
|
||||
#[rpc(client)]
|
||||
pub type SignalCliClient = gen_client::Client;
|
||||
|
||||
#[rpc(client, params = "named")]
|
||||
pub trait Rpc {
|
||||
#[method(name = "addDevice", param_kind = map)]
|
||||
async fn add_device(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
uri: String,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "addDevice", params = "named")]
|
||||
fn add_device(&self, account: Option<String>, uri: String) -> Result<Value>;
|
||||
|
||||
#[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)]
|
||||
#[rpc(name = "block", params = "named")]
|
||||
fn block(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "deleteLocalAccountData", param_kind = map)]
|
||||
#[rpc(name = "deleteLocalAccountData", params = "named")]
|
||||
fn delete_local_account_data(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] ignoreRegistered: Option<bool>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[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>;
|
||||
#[rpc(name = "getUserStatus", params = "named")]
|
||||
fn get_user_status(&self, account: Option<String>, recipients: Vec<String>) -> Result<Value>;
|
||||
|
||||
#[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>;
|
||||
#[rpc(name = "joinGroup", params = "named")]
|
||||
fn join_group(&self, account: Option<String>, uri: String) -> Result<Value>;
|
||||
|
||||
#[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)]
|
||||
fn get_user_status(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
usernames: Vec<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[method(name = "joinGroup", param_kind = map)]
|
||||
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)]
|
||||
#[rpc(name = "finishLink", params = "named")]
|
||||
fn finish_link(
|
||||
&self,
|
||||
#[allow(non_snake_case)] deviceLinkUri: String,
|
||||
#[allow(non_snake_case)] deviceName: Option<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
#[allow(non_snake_case)] deviceName: String,
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "listAccounts", param_kind = map)]
|
||||
fn list_accounts(&self) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "listAccounts", params = "named")]
|
||||
fn list_accounts(&self) -> Result<Value>;
|
||||
|
||||
#[method(name = "listContacts", param_kind = map)]
|
||||
#[rpc(name = "listContacts", params = "named")]
|
||||
fn list_contacts(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
@ -104,68 +51,60 @@ pub trait Rpc {
|
||||
#[allow(non_snake_case)] allRecipients: bool,
|
||||
blocked: Option<bool>,
|
||||
name: Option<String>,
|
||||
detailed: bool,
|
||||
internal: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "listDevices", param_kind = map)]
|
||||
fn list_devices(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "listDevices", params = "named")]
|
||||
fn list_devices(&self, account: Option<String>) -> Result<Value>;
|
||||
|
||||
#[method(name = "listGroups", param_kind = map)]
|
||||
#[rpc(name = "listGroups", params = "named")]
|
||||
fn list_groups(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "listIdentities", param_kind = map)]
|
||||
fn list_identities(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
number: Option<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "listIdentities", params = "named")]
|
||||
fn list_identities(&self, account: Option<String>, number: Option<String>) -> Result<Value>;
|
||||
|
||||
#[method(name = "listStickerPacks", param_kind = map)]
|
||||
fn list_sticker_packs(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "listStickerPacks", params = "named")]
|
||||
fn list_sticker_packs(&self, account: Option<String>) -> Result<Value>;
|
||||
|
||||
#[method(name = "quitGroup", param_kind = map)]
|
||||
#[rpc(name = "quitGroup", params = "named")]
|
||||
fn quit_group(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] groupId: String,
|
||||
delete: bool,
|
||||
admins: Vec<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "register", param_kind = map)]
|
||||
#[rpc(name = "register", params = "named")]
|
||||
fn register(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
voice: bool,
|
||||
captcha: Option<String>,
|
||||
reregister: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "removeContact", param_kind = map)]
|
||||
#[rpc(name = "removeContact", params = "named")]
|
||||
fn remove_contact(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
forget: bool,
|
||||
hide: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "removeDevice", param_kind = map)]
|
||||
#[rpc(name = "removeDevice", params = "named")]
|
||||
fn remove_device(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] deviceId: u32,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "removePin", param_kind = map)]
|
||||
fn remove_pin(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "removePin", params = "named")]
|
||||
fn remove_pin(&self, account: Option<String>) -> Result<Value>;
|
||||
|
||||
#[method(name = "remoteDelete", param_kind = map)]
|
||||
#[rpc(name = "remoteDelete", params = "named")]
|
||||
fn remote_delete(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
@ -173,274 +112,140 @@ pub trait Rpc {
|
||||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
#[allow(non_snake_case)] noteToSelf: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[method(name = "send", param_kind = map)]
|
||||
#[rpc(name = "send", params = "named")]
|
||||
fn send(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
groupIds: Vec<String>,
|
||||
usernames: Vec<String>,
|
||||
#[allow(non_snake_case)] notifySelf: bool,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
#[allow(non_snake_case)] noteToSelf: bool,
|
||||
#[allow(non_snake_case)] endSession: bool,
|
||||
message: String,
|
||||
attachments: Vec<String>,
|
||||
#[allow(non_snake_case)] viewOnce: bool,
|
||||
mentions: Vec<String>,
|
||||
#[allow(non_snake_case)] textStyle: Vec<String>,
|
||||
#[allow(non_snake_case)] quoteTimestamp: Option<u64>,
|
||||
#[allow(non_snake_case)] quoteAuthor: Option<String>,
|
||||
#[allow(non_snake_case)] quoteMessage: Option<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>,
|
||||
#[allow(non_snake_case)] storyTimestamp: Option<u64>,
|
||||
#[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>;
|
||||
|
||||
#[method(name = "sendContacts", param_kind = map)]
|
||||
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "sendContacts", params = "named")]
|
||||
fn send_contacts(&self, account: Option<String>) -> Result<Value>;
|
||||
|
||||
#[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)]
|
||||
#[rpc(name = "sendPaymentNotification", params = "named")]
|
||||
fn send_payment_notification(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
receipt: String,
|
||||
note: String,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "sendReaction", param_kind = map)]
|
||||
#[rpc(name = "sendReaction", params = "named")]
|
||||
fn send_reaction(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
usernames: Vec<String>,
|
||||
#[allow(non_snake_case)] noteToSelf: bool,
|
||||
#[allow(non_snake_case)] notifySelf: bool,
|
||||
emoji: String,
|
||||
#[allow(non_snake_case)] targetAuthor: String,
|
||||
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||
remove: bool,
|
||||
story: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "sendReceipt", param_kind = map)]
|
||||
#[rpc(name = "sendReceipt", params = "named")]
|
||||
fn send_receipt(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
usernames: Vec<String>,
|
||||
#[allow(non_snake_case)] targetTimestamps: Vec<u64>,
|
||||
r#type: String,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "sendSyncRequest", param_kind = map)]
|
||||
fn send_sync_request(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "sendSyncRequest", params = "named")]
|
||||
fn send_sync_request(&self, account: Option<String>) -> Result<Value>;
|
||||
|
||||
#[method(name = "sendTyping", param_kind = map)]
|
||||
#[rpc(name = "sendTyping", params = "named")]
|
||||
fn send_typing(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
stop: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[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>;
|
||||
#[rpc(name = "setPin", params = "named")]
|
||||
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value>;
|
||||
|
||||
#[method(name = "setPin", param_kind = map)]
|
||||
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[method(name = "submitRateLimitChallenge", param_kind = map)]
|
||||
#[rpc(name = "submitRateLimitChallenge", params = "named")]
|
||||
fn submit_rate_limit_challenge(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
challenge: String,
|
||||
captcha: String,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "startChangeNumber", param_kind = map)]
|
||||
fn start_change_number(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
number: String,
|
||||
voice: bool,
|
||||
captcha: Option<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "startLink", params = "named")]
|
||||
fn start_link(&self, account: Option<String>) -> Result<JsonLink>;
|
||||
|
||||
#[method(name = "startLink", param_kind = map)]
|
||||
fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>;
|
||||
|
||||
#[method(name = "trust", param_kind = map)]
|
||||
#[rpc(name = "trust", params = "named")]
|
||||
fn trust(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
#[allow(non_snake_case)] trustAllKnownKeys: bool,
|
||||
#[allow(non_snake_case)] verifiedSafetyNumber: Option<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "unblock", param_kind = map)]
|
||||
#[rpc(name = "unblock", params = "named")]
|
||||
fn unblock(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "unregister", param_kind = map)]
|
||||
#[rpc(name = "unregister", params = "named")]
|
||||
fn unregister(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] deleteAccount: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[method(name = "updateAccount", param_kind = map)]
|
||||
#[rpc(name = "updateAccount", params = "named")]
|
||||
fn update_account(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
deviceName: Option<String>,
|
||||
unrestrictedUnidentifiedSender: Option<bool>,
|
||||
discoverableByNumber: Option<bool>,
|
||||
numberSharing: Option<bool>,
|
||||
username: Option<String>,
|
||||
deleteUsername: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
#[allow(non_snake_case)] deviceName: Option<String>,
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "updateConfiguration", param_kind = map)]
|
||||
#[rpc(name = "updateConfiguration", params = "named")]
|
||||
fn update_configuration(
|
||||
&self,
|
||||
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)] typingIndicators: Option<bool>,
|
||||
#[allow(non_snake_case)] linkPreviews: Option<bool>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "updateContact", param_kind = map)]
|
||||
#[rpc(name = "updateContact", params = "named")]
|
||||
fn update_contact(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
name: Option<String>,
|
||||
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>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[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>;
|
||||
|
||||
#[method(name = "updateGroup", param_kind = map)]
|
||||
#[rpc(name = "updateGroup", params = "named")]
|
||||
fn update_group(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
@ -460,11 +265,9 @@ pub trait Rpc {
|
||||
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
|
||||
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
|
||||
expiration: Option<u32>,
|
||||
#[allow(non_snake_case)] memberLabelEmoji: Option<String>,
|
||||
#[allow(non_snake_case)] memberLabel: Option<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "updateProfile", param_kind = map)]
|
||||
#[rpc(name = "updateProfile", params = "named")]
|
||||
fn update_profile(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
@ -475,33 +278,32 @@ pub trait Rpc {
|
||||
#[allow(non_snake_case)] mobileCoinAddress: Option<String>,
|
||||
avatar: Option<String>,
|
||||
#[allow(non_snake_case)] removeAvatar: bool,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[method(name = "uploadStickerPack", param_kind = map)]
|
||||
fn upload_sticker_pack(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
path: String,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
#[rpc(name = "uploadStickerPack", params = "named")]
|
||||
fn upload_sticker_pack(&self, account: Option<String>, path: String) -> Result<Value>;
|
||||
|
||||
#[method(name = "verify", param_kind = map)]
|
||||
#[rpc(name = "verify", params = "named")]
|
||||
fn verify(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] verificationCode: String,
|
||||
pin: Option<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
) -> Result<Value>;
|
||||
|
||||
#[subscription(
|
||||
name = "subscribeReceive" => "receive",
|
||||
unsubscribe = "unsubscribeReceive",
|
||||
item = Value,
|
||||
param_kind = map
|
||||
#[pubsub(
|
||||
subscription = "receive",
|
||||
subscribe,
|
||||
name = "subscribeReceive",
|
||||
params = "named"
|
||||
)]
|
||||
async fn subscribe_receive(&self, account: Option<String>) -> SubscriptionResult;
|
||||
fn subscribe_receive(&self, _: Self::Metadata, _: Subscriber<Value>, account: Option<String>);
|
||||
|
||||
#[method(name = "version")]
|
||||
fn version(&self) -> Result<Value, ErrorObjectOwned>;
|
||||
#[pubsub(subscription = "receive", unsubscribe, name = "unsubscribeReceive")]
|
||||
fn unsubscribe_receive(&self, _: Option<Self::Metadata>, _: SubscriptionId) -> Result<bool>;
|
||||
|
||||
#[rpc(name = "version")]
|
||||
fn version(&self) -> Result<Value>;
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -510,23 +312,10 @@ pub struct JsonLink {
|
||||
pub device_link_uri: String,
|
||||
}
|
||||
|
||||
pub async fn connect_tcp(
|
||||
tcp: impl ToSocketAddrs,
|
||||
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
||||
let (sender, receiver) = super::transports::tcp::connect(tcp).await?;
|
||||
|
||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||
pub async fn connect_tcp(tcp: impl ToSocketAddrs) -> Result<SignalCliClient, RpcError> {
|
||||
super::tcp::connect::<_, SignalCliClient>(tcp).await
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn connect_unix(
|
||||
socket_path: impl AsRef<Path>,
|
||||
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
||||
let (sender, receiver) = super::transports::ipc::connect(socket_path).await?;
|
||||
|
||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> {
|
||||
HttpClientBuilder::default().build(uri)
|
||||
pub async fn connect_unix(socket_path: impl AsRef<Path>) -> Result<SignalCliClient, RpcError> {
|
||||
ipc::connect::<_, SignalCliClient>(socket_path).await
|
||||
}
|
||||
|
||||
@ -1,112 +1,85 @@
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use clap::Parser;
|
||||
use jsonrpsee::core::client::{Error as RpcError, Subscription, SubscriptionClientT};
|
||||
use serde_json::{Error, Value};
|
||||
use jsonrpc_client_transports::{RpcError, TypedSubscriptionStream};
|
||||
use jsonrpc_core::{futures_util::StreamExt, Value};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use tokio::{select, time::sleep};
|
||||
|
||||
use cli::Cli;
|
||||
|
||||
use crate::cli::{CliCommands, GroupPermission, LinkState};
|
||||
use crate::jsonrpc::RpcClient;
|
||||
use crate::cli::{GroupPermission, LinkState};
|
||||
|
||||
mod cli;
|
||||
#[allow(non_snake_case, clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
mod jsonrpc;
|
||||
mod transports;
|
||||
mod tcp;
|
||||
|
||||
const DEFAULT_TCP: &str = "127.0.0.1:7583";
|
||||
const DEFAULT_SOCKET_SUFFIX: &str = "signal-cli/socket";
|
||||
const DEFAULT_HTTP: &str = "http://localhost:8080/api/v1/rpc";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let cli = cli::Cli::parse();
|
||||
|
||||
let result = connect(cli).await;
|
||||
let client = connect(&cli)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to socket: {e}"))?;
|
||||
|
||||
match result {
|
||||
Ok(Value::Null) => {}
|
||||
Ok(v) => println!("{v}"),
|
||||
Err(e) => return Err(anyhow::anyhow!("JSON-RPC command failed: {e:?}")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_command(
|
||||
cli: Cli,
|
||||
client: impl SubscriptionClientT + Sync,
|
||||
) -> Result<Value, RpcError> {
|
||||
match cli.command {
|
||||
CliCommands::Receive { timeout } => {
|
||||
let mut stream = client.subscribe_receive(cli.account).await?;
|
||||
let result = match cli.command {
|
||||
cli::CliCommands::Receive { timeout } => {
|
||||
let mut stream = client
|
||||
.subscribe_receive(cli.account)
|
||||
.map_err(|e| anyhow::anyhow!("JSON-RPC command failed: {:?}", e))?;
|
||||
|
||||
{
|
||||
while let Some(v) = stream_next(timeout, &mut stream).await {
|
||||
let v = v?;
|
||||
let v = v.map_err(|e| anyhow::anyhow!("JSON-RPC command failed: {:?}", e))?;
|
||||
println!("{v}");
|
||||
}
|
||||
}
|
||||
stream.unsubscribe().await?;
|
||||
Ok(Value::Null)
|
||||
return Ok(());
|
||||
}
|
||||
CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
|
||||
CliCommands::Block {
|
||||
cli::CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
|
||||
cli::CliCommands::Block {
|
||||
recipient,
|
||||
group_id,
|
||||
} => client.block(cli.account, recipient, group_id).await,
|
||||
CliCommands::DeleteLocalAccountData { ignore_registered } => {
|
||||
cli::CliCommands::DeleteLocalAccountData { ignore_registered } => {
|
||||
client
|
||||
.delete_local_account_data(cli.account, ignore_registered)
|
||||
.await
|
||||
}
|
||||
CliCommands::GetUserStatus {
|
||||
recipient,
|
||||
username,
|
||||
} => {
|
||||
client
|
||||
.get_user_status(cli.account, recipient, username)
|
||||
.await
|
||||
cli::CliCommands::GetUserStatus { recipient } => {
|
||||
client.get_user_status(cli.account, recipient).await
|
||||
}
|
||||
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
||||
CliCommands::Link { name } => {
|
||||
cli::CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
||||
cli::CliCommands::Link { name } => {
|
||||
let url = client
|
||||
.start_link(cli.account)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
||||
.map_err(|e| anyhow::anyhow!("JSON-RPC command startLink failed: {e:?}",))?
|
||||
.device_link_uri;
|
||||
println!("{url}");
|
||||
println!("{}", url);
|
||||
client.finish_link(url, name).await
|
||||
}
|
||||
CliCommands::ListAccounts => client.list_accounts().await,
|
||||
CliCommands::ListContacts {
|
||||
cli::CliCommands::ListAccounts => client.list_accounts().await,
|
||||
cli::CliCommands::ListContacts {
|
||||
recipient,
|
||||
all_recipients,
|
||||
blocked,
|
||||
name,
|
||||
detailed,
|
||||
internal,
|
||||
} => {
|
||||
client
|
||||
.list_contacts(
|
||||
cli.account,
|
||||
recipient,
|
||||
all_recipients,
|
||||
blocked,
|
||||
name,
|
||||
detailed,
|
||||
internal,
|
||||
)
|
||||
.list_contacts(cli.account, recipient, all_recipients, blocked, name)
|
||||
.await
|
||||
}
|
||||
CliCommands::ListDevices => client.list_devices(cli.account).await,
|
||||
CliCommands::ListGroups {
|
||||
cli::CliCommands::ListDevices => client.list_devices(cli.account).await,
|
||||
cli::CliCommands::ListGroups {
|
||||
detailed: _,
|
||||
group_id,
|
||||
} => client.list_groups(cli.account, group_id).await,
|
||||
CliCommands::ListIdentities { number } => client.list_identities(cli.account, number).await,
|
||||
CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
|
||||
CliCommands::QuitGroup {
|
||||
cli::CliCommands::ListIdentities { number } => {
|
||||
client.list_identities(cli.account, number).await
|
||||
}
|
||||
cli::CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
|
||||
cli::CliCommands::QuitGroup {
|
||||
group_id,
|
||||
delete,
|
||||
admin,
|
||||
@ -115,29 +88,17 @@ async fn handle_command(
|
||||
.quit_group(cli.account, group_id, delete, admin)
|
||||
.await
|
||||
}
|
||||
CliCommands::Register {
|
||||
voice,
|
||||
captcha,
|
||||
reregister,
|
||||
} => {
|
||||
client
|
||||
.register(cli.account, voice, captcha, reregister)
|
||||
.await
|
||||
cli::CliCommands::Register { voice, captcha } => {
|
||||
client.register(cli.account, voice, captcha).await
|
||||
}
|
||||
CliCommands::RemoveContact {
|
||||
recipient,
|
||||
forget,
|
||||
hide,
|
||||
} => {
|
||||
client
|
||||
.remove_contact(cli.account, recipient, forget, hide)
|
||||
.await
|
||||
cli::CliCommands::RemoveContact { recipient, forget } => {
|
||||
client.remove_contact(cli.account, recipient, forget).await
|
||||
}
|
||||
CliCommands::RemoveDevice { device_id } => {
|
||||
cli::CliCommands::RemoveDevice { device_id } => {
|
||||
client.remove_device(cli.account, device_id).await
|
||||
}
|
||||
CliCommands::RemovePin => client.remove_pin(cli.account).await,
|
||||
CliCommands::RemoteDelete {
|
||||
cli::CliCommands::RemovePin => client.remove_pin(cli.account).await,
|
||||
cli::CliCommands::RemoteDelete {
|
||||
target_timestamp,
|
||||
recipient,
|
||||
group_id,
|
||||
@ -153,91 +114,44 @@ async fn handle_command(
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommands::Send {
|
||||
cli::CliCommands::Send {
|
||||
recipient,
|
||||
group_id,
|
||||
username,
|
||||
notify_self,
|
||||
note_to_self,
|
||||
end_session,
|
||||
message,
|
||||
message_from_stdin,
|
||||
attachment,
|
||||
view_once,
|
||||
mention,
|
||||
text_style,
|
||||
quote_timestamp,
|
||||
quote_author,
|
||||
quote_message,
|
||||
quote_mention,
|
||||
quote_text_style,
|
||||
quote_attachment,
|
||||
preview_url,
|
||||
preview_title,
|
||||
preview_description,
|
||||
preview_image,
|
||||
sticker,
|
||||
story_timestamp,
|
||||
story_author,
|
||||
edit_timestamp,
|
||||
no_urgent,
|
||||
} => {
|
||||
client
|
||||
.send(
|
||||
cli.account,
|
||||
recipient,
|
||||
group_id,
|
||||
username,
|
||||
notify_self,
|
||||
note_to_self,
|
||||
end_session,
|
||||
if message_from_stdin {
|
||||
std::io::read_to_string(std::io::stdin()).unwrap()
|
||||
} else {
|
||||
message.unwrap_or_default()
|
||||
},
|
||||
message.unwrap_or_default(),
|
||||
attachment,
|
||||
view_once,
|
||||
mention,
|
||||
text_style,
|
||||
quote_timestamp,
|
||||
quote_author,
|
||||
quote_message,
|
||||
quote_mention,
|
||||
quote_text_style,
|
||||
quote_attachment,
|
||||
preview_url,
|
||||
preview_title,
|
||||
preview_description,
|
||||
preview_image,
|
||||
sticker,
|
||||
story_timestamp,
|
||||
story_author,
|
||||
edit_timestamp,
|
||||
no_urgent,
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommands::SendContacts => client.send_contacts(cli.account).await,
|
||||
CliCommands::SendAdminDelete {
|
||||
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 {
|
||||
cli::CliCommands::SendContacts => client.send_contacts(cli.account).await,
|
||||
cli::CliCommands::SendPaymentNotification {
|
||||
recipient,
|
||||
receipt,
|
||||
note,
|
||||
@ -246,108 +160,10 @@ async fn handle_command(
|
||||
.send_payment_notification(cli.account, recipient, receipt, note)
|
||||
.await
|
||||
}
|
||||
CliCommands::SendPinMessage {
|
||||
cli::CliCommands::SendReaction {
|
||||
recipient,
|
||||
group_id,
|
||||
username,
|
||||
target_author,
|
||||
target_timestamp,
|
||||
pin_duration,
|
||||
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,
|
||||
target_author,
|
||||
target_timestamp,
|
||||
@ -359,9 +175,7 @@ async fn handle_command(
|
||||
cli.account,
|
||||
recipient,
|
||||
group_id,
|
||||
username,
|
||||
note_to_self,
|
||||
notify_self,
|
||||
emoji,
|
||||
target_author,
|
||||
target_timestamp,
|
||||
@ -370,9 +184,8 @@ async fn handle_command(
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommands::SendReceipt {
|
||||
cli::CliCommands::SendReceipt {
|
||||
recipient,
|
||||
username,
|
||||
target_timestamp,
|
||||
r#type,
|
||||
} => {
|
||||
@ -380,7 +193,6 @@ async fn handle_command(
|
||||
.send_receipt(
|
||||
cli.account,
|
||||
recipient,
|
||||
username,
|
||||
target_timestamp,
|
||||
match r#type {
|
||||
cli::ReceiptType::Read => "read".to_owned(),
|
||||
@ -389,8 +201,8 @@ async fn handle_command(
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
|
||||
CliCommands::SendTyping {
|
||||
cli::CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
|
||||
cli::CliCommands::SendTyping {
|
||||
recipient,
|
||||
group_id,
|
||||
stop,
|
||||
@ -399,37 +211,13 @@ async fn handle_command(
|
||||
.send_typing(cli.account, recipient, group_id, stop)
|
||||
.await
|
||||
}
|
||||
CliCommands::SendUnpinMessage {
|
||||
recipient,
|
||||
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 } => {
|
||||
cli::CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
|
||||
cli::CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
|
||||
client
|
||||
.submit_rate_limit_challenge(cli.account, challenge, captcha)
|
||||
.await
|
||||
}
|
||||
CliCommands::Trust {
|
||||
cli::CliCommands::Trust {
|
||||
recipient,
|
||||
trust_all_known_keys,
|
||||
verified_safety_number,
|
||||
@ -443,34 +231,17 @@ async fn handle_command(
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommands::Unblock {
|
||||
cli::CliCommands::Unblock {
|
||||
recipient,
|
||||
group_id,
|
||||
} => client.unblock(cli.account, recipient, group_id).await,
|
||||
CliCommands::Unregister { delete_account } => {
|
||||
cli::CliCommands::Unregister { delete_account } => {
|
||||
client.unregister(cli.account, delete_account).await
|
||||
}
|
||||
CliCommands::UpdateAccount {
|
||||
device_name,
|
||||
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
|
||||
cli::CliCommands::UpdateAccount { device_name } => {
|
||||
client.update_account(cli.account, device_name).await
|
||||
}
|
||||
CliCommands::UpdateConfiguration {
|
||||
cli::CliCommands::UpdateConfiguration {
|
||||
read_receipts,
|
||||
unidentified_delivery_indicators,
|
||||
typing_indicators,
|
||||
@ -486,39 +257,16 @@ async fn handle_command(
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommands::UpdateContact {
|
||||
cli::CliCommands::UpdateContact {
|
||||
recipient,
|
||||
expiration,
|
||||
name,
|
||||
given_name,
|
||||
family_name,
|
||||
nick_given_name,
|
||||
nick_family_name,
|
||||
note,
|
||||
} => {
|
||||
client
|
||||
.update_contact(
|
||||
cli.account,
|
||||
recipient,
|
||||
name,
|
||||
expiration,
|
||||
given_name,
|
||||
family_name,
|
||||
nick_given_name,
|
||||
nick_family_name,
|
||||
note,
|
||||
)
|
||||
.update_contact(cli.account, recipient, name, expiration)
|
||||
.await
|
||||
}
|
||||
CliCommands::UpdateDevice {
|
||||
device_id,
|
||||
device_name,
|
||||
} => {
|
||||
client
|
||||
.update_device(cli.account, device_id, device_name)
|
||||
.await
|
||||
}
|
||||
CliCommands::UpdateGroup {
|
||||
cli::CliCommands::UpdateGroup {
|
||||
group_id,
|
||||
name,
|
||||
description,
|
||||
@ -535,8 +283,6 @@ async fn handle_command(
|
||||
set_permission_edit_details,
|
||||
set_permission_send_messages,
|
||||
expiration,
|
||||
member_label_emoji,
|
||||
member_label,
|
||||
} => {
|
||||
client
|
||||
.update_group(
|
||||
@ -570,12 +316,10 @@ async fn handle_command(
|
||||
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
||||
}),
|
||||
expiration,
|
||||
member_label_emoji,
|
||||
member_label,
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommands::UpdateProfile {
|
||||
cli::CliCommands::UpdateProfile {
|
||||
given_name,
|
||||
family_name,
|
||||
about,
|
||||
@ -597,126 +341,47 @@ async fn handle_command(
|
||||
)
|
||||
.await
|
||||
}
|
||||
CliCommands::UploadStickerPack { path } => {
|
||||
cli::CliCommands::UploadStickerPack { path } => {
|
||||
client.upload_sticker_pack(cli.account, path).await
|
||||
}
|
||||
CliCommands::Verify {
|
||||
cli::CliCommands::Verify {
|
||||
verification_code,
|
||||
pin,
|
||||
} => client.verify(cli.account, verification_code, pin).await,
|
||||
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
|
||||
}
|
||||
}
|
||||
cli::CliCommands::Version => client.version().await,
|
||||
};
|
||||
|
||||
result
|
||||
.map(|v| println!("{v}"))
|
||||
.map_err(|e| anyhow::anyhow!("JSON-RPC command failed: {e:?}",))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect(cli: Cli) -> Result<Value, RpcError> {
|
||||
if let Some(http) = &cli.json_rpc_http {
|
||||
let uri = if let Some(uri) = http {
|
||||
uri
|
||||
} else {
|
||||
DEFAULT_HTTP
|
||||
};
|
||||
let client = jsonrpc::connect_http(uri)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
|
||||
handle_command(cli, client).await
|
||||
} else if let Some(tcp) = cli.json_rpc_tcp {
|
||||
async fn connect(cli: &cli::Cli) -> Result<jsonrpc::SignalCliClient, RpcError> {
|
||||
if let Some(tcp) = cli.json_rpc_tcp {
|
||||
let socket_addr = tcp.unwrap_or_else(|| DEFAULT_TCP.parse().unwrap());
|
||||
let client = jsonrpc::connect_tcp(socket_addr)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
|
||||
handle_command(cli, client).await
|
||||
jsonrpc::connect_tcp(socket_addr).await
|
||||
} else {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Err(RpcError::Custom("Invalid socket".into()))
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let socket_path = cli
|
||||
.json_rpc_socket
|
||||
.clone()
|
||||
.unwrap_or(None)
|
||||
.or_else(|| {
|
||||
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
||||
PathBuf::from(runtime_dir)
|
||||
.join(DEFAULT_SOCKET_SUFFIX)
|
||||
.into()
|
||||
})
|
||||
let socket_path = cli
|
||||
.json_rpc_socket
|
||||
.clone()
|
||||
.unwrap_or(None)
|
||||
.or_else(|| {
|
||||
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
||||
PathBuf::from(runtime_dir)
|
||||
.join(DEFAULT_SOCKET_SUFFIX)
|
||||
.into()
|
||||
})
|
||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
||||
let client = jsonrpc::connect_unix(socket_path)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
|
||||
handle_command(cli, client).await
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
||||
jsonrpc::connect_unix(socket_path).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_next(
|
||||
timeout: f64,
|
||||
stream: &mut Subscription<Value>,
|
||||
) -> Option<Result<Value, Error>> {
|
||||
stream: &mut TypedSubscriptionStream<Value>,
|
||||
) -> Option<Result<Value, RpcError>> {
|
||||
if timeout < 0.0 {
|
||||
stream.next().await
|
||||
} else {
|
||||
|
||||
29
client/src/tcp.rs
Normal file
29
client/src/tcp.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use jsonrpc_client_transports::{transports::duplex, RpcChannel, RpcError};
|
||||
use jsonrpc_core::futures_util::{SinkExt, StreamExt, TryStreamExt};
|
||||
use jsonrpc_server_utils::{codecs::StreamCodec, tokio_util::codec::Decoder};
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
|
||||
/// Connect to a JSON-RPC TCP server.
|
||||
pub async fn connect<S: ToSocketAddrs, Client: From<RpcChannel>>(
|
||||
socket: S,
|
||||
) -> Result<Client, RpcError> {
|
||||
let connection = TcpStream::connect(socket)
|
||||
.await
|
||||
.map_err(|e| RpcError::Other(Box::new(e)))?;
|
||||
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
|
||||
let sink = sink.sink_map_err(|e| RpcError::Other(Box::new(e)));
|
||||
let stream = stream.map_err(|e| log::error!("TCP stream error: {}", e));
|
||||
|
||||
let (client, sender) = duplex(
|
||||
Box::pin(sink),
|
||||
Box::pin(
|
||||
stream
|
||||
.take_while(|x| std::future::ready(x.is_ok()))
|
||||
.map(|x| x.expect("Stream is closed upon first error.")),
|
||||
),
|
||||
);
|
||||
|
||||
tokio::spawn(client);
|
||||
|
||||
Ok(sender.into())
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
use std::io::Error;
|
||||
use std::path::Path;
|
||||
|
||||
use futures_util::stream::StreamExt;
|
||||
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
||||
use tokio::net::UnixStream;
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
use super::stream_codec::StreamCodec;
|
||||
use super::{Receiver, Sender};
|
||||
|
||||
/// Connect to a JSON-RPC Unix Socket server.
|
||||
pub async fn connect(
|
||||
socket: impl AsRef<Path>,
|
||||
) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
|
||||
let connection = UnixStream::connect(socket).await?;
|
||||
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
|
||||
|
||||
let sender = Sender { inner: sink };
|
||||
let receiver = Receiver { inner: stream };
|
||||
|
||||
Ok((sender, receiver))
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
||||
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod ipc;
|
||||
mod stream_codec;
|
||||
pub mod tcp;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum Errors {
|
||||
#[error("Other: {0}")]
|
||||
Other(String),
|
||||
#[error("Closed")]
|
||||
Closed,
|
||||
}
|
||||
|
||||
struct Sender<T: Send + Sink<String>> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
||||
for Sender<T>
|
||||
{
|
||||
type Error = Errors;
|
||||
|
||||
async fn send(&mut self, body: String) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.send(body)
|
||||
.await
|
||||
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn close(&mut self) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.close()
|
||||
.await
|
||||
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Receiver<T: Send + Stream> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
||||
for Receiver<T>
|
||||
{
|
||||
type Error = Errors;
|
||||
|
||||
async fn receive(&mut self) -> Result<ReceivedMessage, Self::Error> {
|
||||
match self.inner.next().await {
|
||||
None => Err(Errors::Closed),
|
||||
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
||||
Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
use bytes::BytesMut;
|
||||
use std::{io, str};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
type Separator = u8;
|
||||
|
||||
/// Stream codec for streaming protocols (ipc, tcp)
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StreamCodec {
|
||||
incoming_separator: Separator,
|
||||
outgoing_separator: Separator,
|
||||
}
|
||||
|
||||
impl StreamCodec {
|
||||
/// Default codec with streaming input data. Input can be both enveloped and not.
|
||||
pub fn stream_incoming() -> Self {
|
||||
StreamCodec::new(b'\n', b'\n')
|
||||
}
|
||||
|
||||
/// New custom stream codec
|
||||
pub fn new(incoming_separator: Separator, outgoing_separator: Separator) -> Self {
|
||||
StreamCodec {
|
||||
incoming_separator,
|
||||
outgoing_separator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for StreamCodec {
|
||||
type Item = String;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, buf: &mut BytesMut) -> io::Result<Option<Self::Item>> {
|
||||
if let Some(i) = buf
|
||||
.as_ref()
|
||||
.iter()
|
||||
.position(|&b| b == self.incoming_separator)
|
||||
{
|
||||
let line = buf.split_to(i);
|
||||
let _ = buf.split_to(1);
|
||||
|
||||
match str::from_utf8(line.as_ref()) {
|
||||
Ok(s) => Ok(Some(s.to_string())),
|
||||
Err(_) => Err(io::Error::other("invalid UTF-8")),
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<String> for StreamCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, msg: String, buf: &mut BytesMut) -> io::Result<()> {
|
||||
let mut payload = msg.into_bytes();
|
||||
payload.push(self.outgoing_separator);
|
||||
buf.extend_from_slice(&payload);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
use std::io::Error;
|
||||
|
||||
use futures_util::stream::StreamExt;
|
||||
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
use super::stream_codec::StreamCodec;
|
||||
use super::{Receiver, Sender};
|
||||
|
||||
/// Connect to a JSON-RPC TCP server.
|
||||
pub async fn connect(
|
||||
socket: impl ToSocketAddrs,
|
||||
) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
|
||||
let connection = TcpStream::connect(socket).await?;
|
||||
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
|
||||
|
||||
let sender = Sender { inner: sink };
|
||||
let receiver = Receiver { inner: stream };
|
||||
|
||||
Ok((sender, receiver))
|
||||
}
|
||||
@ -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>
|
||||
@ -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 |
@ -32,6 +32,8 @@ RestrictAddressFamilies=AF_INET AF_INET6
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
# JVM always exits with 143 in reaction to SIGTERM signal
|
||||
SuccessExitStatus=143
|
||||
StandardInput=socket
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
@ -8,9 +8,11 @@ After=network-online.target
|
||||
[Service]
|
||||
Type=dbus
|
||||
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
|
||||
BusName=org.asamk.Signal
|
||||
# JVM always exits with 143 in reaction to SIGTERM signal
|
||||
SuccessExitStatus=143
|
||||
|
||||
[Install]
|
||||
Alias=dbus-org.asamk.Signal.service
|
||||
|
||||
@ -8,9 +8,11 @@ After=network-online.target
|
||||
[Service]
|
||||
Type=dbus
|
||||
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
|
||||
BusName=org.asamk.Signal
|
||||
# JVM always exits with 143 in reaction to SIGTERM signal
|
||||
SuccessExitStatus=143
|
||||
|
||||
[Install]
|
||||
Alias=dbus-org.asamk.Signal.service
|
||||
|
||||
@ -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.
|
||||
|
||||
281
graalvm-config-dir/jni-config.json
Normal file
281
graalvm-config-dir/jni-config.json
Normal file
@ -0,0 +1,281 @@
|
||||
[
|
||||
{
|
||||
"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.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":"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":"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.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.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"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
8
graalvm-config-dir/predefined-classes-config.json
Normal file
8
graalvm-config-dir/predefined-classes-config.json
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"type":"agent-extracted",
|
||||
"classes":[
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
23
graalvm-config-dir/proxy-config.json
Normal file
23
graalvm-config-dir/proxy-config.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
4032
graalvm-config-dir/reflect-config.json
Normal file
4032
graalvm-config-dir/reflect-config.json
Normal file
File diff suppressed because it is too large
Load Diff
244
graalvm-config-dir/resource-config.json
Normal file
244
graalvm-config-dir/resource-config.json
Normal file
@ -0,0 +1,244 @@
|
||||
{
|
||||
"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/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":"\\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":"\\Qsqlite-jdbc.properties\\E"
|
||||
},
|
||||
{
|
||||
"pattern":"com/google/i18n/phonenumbers/data/.*"
|
||||
}
|
||||
]},
|
||||
"bundles":[{
|
||||
"name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl",
|
||||
"locales":[
|
||||
"",
|
||||
"en",
|
||||
"und"
|
||||
]
|
||||
}]
|
||||
}
|
||||
8
graalvm-config-dir/serialization-config.json
Normal file
8
graalvm-config-dir/serialization-config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"types":[
|
||||
],
|
||||
"lambdaCapturingTypes":[
|
||||
],
|
||||
"proxies":[
|
||||
]
|
||||
}
|
||||
@ -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" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,9 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
37
gradlew
vendored
37
gradlew
vendored
@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -15,12 +15,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# 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:
|
||||
#
|
||||
@ -29,7 +27,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh gradlew
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
@ -57,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (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.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@ -85,8 +83,7 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# 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:-./}" && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@ -114,6 +111,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@ -132,13 +130,10 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@ -146,7 +141,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# 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 ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@ -154,7 +149,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# 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" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@ -171,6 +166,7 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@ -202,15 +198,16 @@ fi
|
||||
# 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"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
58
gradlew.bat
vendored
58
gradlew.bat
vendored
@ -13,18 +13,16 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem gradlew startup script for Windows
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@ -45,13 +43,13 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@ -59,24 +57,36 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@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
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
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
|
||||
|
||||
@ -4,37 +4,20 @@ plugins {
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_25
|
||||
targetCompatibility = JavaVersion.VERSION_25
|
||||
|
||||
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
|
||||
}
|
||||
}
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString()
|
||||
|
||||
dependencies {
|
||||
if (libsignalClientPath == null) {
|
||||
implementation(libs.signalnetwork)
|
||||
} else {
|
||||
implementation(libs.signalnetwork) {
|
||||
exclude(group = "org.signal", module = "libsignal-client")
|
||||
}
|
||||
implementation(files(libsignalClientPath))
|
||||
}
|
||||
implementation(libs.signalservice)
|
||||
implementation(libs.jackson.databind)
|
||||
implementation(libs.protobuf)
|
||||
implementation(libs.bouncycastle)
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.sqlite)
|
||||
implementation(libs.hikari)
|
||||
compileOnly(libs.kotlinx.coroutines.core)
|
||||
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testImplementation(platform(libs.junit.jupiter.bom))
|
||||
testRuntimeOnly(libs.junit.launcher)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
tasks.named<Test>("test") {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package org.asamk.signal.manager.internal;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.helper.AccountFileUpdater;
|
||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
|
||||
public class AccountFileUpdaterImpl implements AccountFileUpdater {
|
||||
class AccountFileUpdaterImpl implements AccountFileUpdater {
|
||||
|
||||
private final AccountsStore accountsStore;
|
||||
private final String accountPath;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.asamk.signal.manager.storage;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.asamk.signal.manager.util.MimeUtils;
|
||||
@ -22,8 +22,7 @@ public class AttachmentStore {
|
||||
}
|
||||
|
||||
public void storeAttachmentPreview(
|
||||
final SignalServiceAttachmentPointer pointer,
|
||||
final AttachmentStorer storer
|
||||
final SignalServiceAttachmentPointer pointer, final AttachmentStorer storer
|
||||
) throws IOException {
|
||||
storeAttachment(getAttachmentPreviewFile(pointer.getRemoteId(),
|
||||
pointer.getFileName(),
|
||||
@ -31,8 +30,7 @@ public class AttachmentStore {
|
||||
}
|
||||
|
||||
public void storeAttachment(
|
||||
final SignalServiceAttachmentPointer pointer,
|
||||
final AttachmentStorer storer
|
||||
final SignalServiceAttachmentPointer pointer, final AttachmentStorer storer
|
||||
) throws IOException {
|
||||
storeAttachment(getAttachmentFile(pointer), storer);
|
||||
}
|
||||
@ -44,8 +42,7 @@ public class AttachmentStore {
|
||||
}
|
||||
|
||||
public StreamDetails retrieveAttachment(final String id) throws IOException {
|
||||
final var safeId = sanitizeId(id);
|
||||
final var attachmentFile = new File(attachmentsPath, safeId);
|
||||
final var attachmentFile = new File(attachmentsPath, id);
|
||||
return Utils.createStreamDetailsFromFile(attachmentFile);
|
||||
}
|
||||
|
||||
@ -57,33 +54,22 @@ public class AttachmentStore {
|
||||
}
|
||||
|
||||
private File getAttachmentPreviewFile(
|
||||
SignalServiceAttachmentRemoteId attachmentId,
|
||||
Optional<String> filename,
|
||||
Optional<String> contentType
|
||||
SignalServiceAttachmentRemoteId attachmentId, Optional<String> filename, Optional<String> contentType
|
||||
) {
|
||||
final var extension = getAttachmentExtension(filename, contentType);
|
||||
final var safe = sanitizeId(attachmentId.toString());
|
||||
return new File(attachmentsPath, safe + extension + ".preview");
|
||||
return new File(attachmentsPath, attachmentId.toString() + extension + ".preview");
|
||||
}
|
||||
|
||||
private File getAttachmentFile(
|
||||
SignalServiceAttachmentRemoteId attachmentId,
|
||||
Optional<String> filename,
|
||||
Optional<String> contentType
|
||||
SignalServiceAttachmentRemoteId attachmentId, Optional<String> filename, Optional<String> contentType
|
||||
) {
|
||||
final var extension = getAttachmentExtension(filename, contentType);
|
||||
final var safe = sanitizeId(attachmentId.toString());
|
||||
return new File(attachmentsPath, safe + extension);
|
||||
return new File(attachmentsPath, attachmentId.toString() + extension);
|
||||
}
|
||||
|
||||
private static String sanitizeId(final String id) {
|
||||
if (id == null) {
|
||||
return "";
|
||||
}
|
||||
return id.replaceAll("[^A-Za-z0-9_.-]", "_");
|
||||
}
|
||||
|
||||
private static String getAttachmentExtension(final Optional<String> filename, final Optional<String> contentType) {
|
||||
private static String getAttachmentExtension(
|
||||
final Optional<String> filename, final Optional<String> contentType
|
||||
) {
|
||||
return filename.filter(f -> f.contains("."))
|
||||
.map(f -> f.substring(f.lastIndexOf(".") + 1))
|
||||
.or(() -> contentType.flatMap(MimeUtils::guessExtensionFromMimeType))
|
||||
@ -1,6 +1,6 @@
|
||||
package org.asamk.signal.manager.storage;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
@ -1,7 +1,9 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
|
||||
import java.net.URI;
|
||||
@ -12,9 +14,9 @@ import java.util.Base64;
|
||||
|
||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||
|
||||
public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
|
||||
public record DeviceLinkInfo(String deviceIdentifier, ECPublicKey deviceKey) {
|
||||
|
||||
public static DeviceLinkUrl parseDeviceLinkUri(URI linkUri) throws InvalidDeviceLinkException {
|
||||
public static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws InvalidDeviceLinkException {
|
||||
final var rawQuery = linkUri.getRawQuery();
|
||||
if (isEmpty(rawQuery)) {
|
||||
throw new RuntimeException("Invalid device link uri");
|
||||
@ -36,12 +38,12 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
|
||||
}
|
||||
ECPublicKey deviceKey;
|
||||
try {
|
||||
deviceKey = new ECPublicKey(publicKeyBytes);
|
||||
deviceKey = Curve.decodePoint(publicKeyBytes, 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidDeviceLinkException("Invalid device link", e);
|
||||
}
|
||||
|
||||
return new DeviceLinkUrl(deviceIdentifier, deviceKey);
|
||||
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
|
||||
}
|
||||
|
||||
public URI createDeviceLinkUri() {
|
||||
17
lib/src/main/java/org/asamk/signal/manager/JobExecutor.java
Normal file
17
lib/src/main/java/org/asamk/signal/manager/JobExecutor.java
Normal file
@ -0,0 +1,17 @@
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.helper.Context;
|
||||
import org.asamk.signal.manager.jobs.Job;
|
||||
|
||||
public class JobExecutor {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public JobExecutor(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void enqueueJob(Job job) {
|
||||
job.run(context);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package org.asamk.signal.manager.internal;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLogger;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
@ -7,9 +7,9 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
public class LibSignalLogger implements SignalProtocolLogger {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger("LibSignal");
|
||||
private final static Logger logger = LoggerFactory.getLogger("LibSignal");
|
||||
|
||||
public static void initLogger() {
|
||||
static void initLogger() {
|
||||
SignalProtocolLoggerProvider.setProvider(new LibSignalLogger());
|
||||
}
|
||||
|
||||
@ -1,67 +1,47 @@
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||
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.Device;
|
||||
import org.asamk.signal.manager.api.DeviceLimitExceededException;
|
||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||
import org.asamk.signal.manager.api.Group;
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.api.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.api.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
||||
import org.asamk.signal.manager.api.Identity;
|
||||
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
||||
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.InvalidStickerException;
|
||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||
import org.asamk.signal.manager.api.LastGroupAdminException;
|
||||
import org.asamk.signal.manager.api.Message;
|
||||
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.NotPrimaryDeviceException;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
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.Recipient;
|
||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||
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.StickerPack;
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
||||
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.UnregisteredRecipientException;
|
||||
import org.asamk.signal.manager.api.UpdateGroup;
|
||||
import org.asamk.signal.manager.api.UpdateProfile;
|
||||
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.LoggerFactory;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
|
||||
import org.asamk.signal.manager.groups.LastGroupAdminException;
|
||||
import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
||||
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@ -72,32 +52,11 @@ import java.util.Set;
|
||||
public interface Manager extends Closeable {
|
||||
|
||||
static boolean isValidNumber(final String e164Number, final String countryCode) {
|
||||
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
||||
}
|
||||
|
||||
static boolean isValidAci(final String aci) {
|
||||
return UuidUtil.INSTANCE.isUuid(aci);
|
||||
}
|
||||
|
||||
static boolean isSignalClientAvailable() {
|
||||
final Logger logger = LoggerFactory.getLogger(Manager.class);
|
||||
try {
|
||||
try {
|
||||
org.signal.libsignal.internal.Native.UuidCiphertext_CheckValidContents(new byte[0]);
|
||||
} catch (Exception e) {
|
||||
logger.trace("Expected exception when checking libsignal-client: {}", e.getMessage());
|
||||
}
|
||||
return true;
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
logger.warn("Failed to call libsignal-client: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
|
||||
}
|
||||
|
||||
String getSelfNumber();
|
||||
|
||||
String getSelfACI();
|
||||
|
||||
/**
|
||||
* This is used for checking a set of phone numbers for registration on Signal
|
||||
*
|
||||
@ -105,20 +64,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.
|
||||
* @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,
|
||||
Boolean unrestrictedUnidentifiedSender,
|
||||
final Boolean discoverableByNumber,
|
||||
final Boolean numberSharing
|
||||
) throws IOException;
|
||||
void updateAccountAttributes(String deviceName) throws IOException;
|
||||
|
||||
Configuration getConfiguration();
|
||||
|
||||
void updateConfiguration(Configuration configuration) throws NotPrimaryDeviceException;
|
||||
void updateConfiguration(Configuration configuration) throws IOException, NotPrimaryDeviceException;
|
||||
|
||||
/**
|
||||
* Update the user's profile.
|
||||
@ -126,15 +78,11 @@ public interface Manager extends Closeable {
|
||||
*/
|
||||
void updateProfile(UpdateProfile updateProfile) throws IOException;
|
||||
|
||||
String getUsername();
|
||||
|
||||
UsernameLinkUrl getUsernameLink();
|
||||
|
||||
/**
|
||||
* Set a username for the account.
|
||||
* 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.
|
||||
@ -142,61 +90,36 @@ public interface Manager extends Closeable {
|
||||
*/
|
||||
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 deleteAccount() throws IOException;
|
||||
|
||||
void submitRateLimitRecaptchaChallenge(
|
||||
String challenge,
|
||||
String captcha
|
||||
) throws IOException, CaptchaRejectedException;
|
||||
void submitRateLimitRecaptchaChallenge(String challenge, String captcha) 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, DeviceLimitExceededException;
|
||||
void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException;
|
||||
|
||||
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException;
|
||||
|
||||
Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
|
||||
|
||||
List<Group> getGroups();
|
||||
|
||||
List<Group> getGroups(Collection<GroupId> groupIds);
|
||||
|
||||
SendGroupMessageResults quitGroup(
|
||||
GroupId groupId,
|
||||
Set<RecipientIdentifier.Single> groupAdmins
|
||||
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
|
||||
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
|
||||
|
||||
void deleteGroup(GroupId groupId) throws IOException;
|
||||
|
||||
SendGroupMessageResults terminateGroup(
|
||||
GroupId groupId
|
||||
) throws IOException, GroupNotFoundException, NotAGroupMemberException;
|
||||
|
||||
Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String name,
|
||||
Set<RecipientIdentifier.Single> members,
|
||||
String avatarFile
|
||||
String name, Set<RecipientIdentifier.Single> members, String avatarFile
|
||||
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
|
||||
|
||||
SendGroupMessageResults updateGroup(
|
||||
final GroupId groupId,
|
||||
final UpdateGroup updateGroup
|
||||
final GroupId groupId, final UpdateGroup updateGroup
|
||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||
|
||||
Pair<GroupId, SendGroupMessageResults> joinGroup(
|
||||
@ -204,42 +127,27 @@ public interface Manager extends Closeable {
|
||||
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
|
||||
|
||||
SendMessageResults sendTypingMessage(
|
||||
TypingAction action,
|
||||
Set<RecipientIdentifier> recipients
|
||||
TypingAction action, Set<RecipientIdentifier> recipients
|
||||
) 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(
|
||||
Message message,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
boolean notifySelf
|
||||
Message message, Set<RecipientIdentifier> recipients
|
||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||
|
||||
SendMessageResults sendEditMessage(
|
||||
Message message,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
long editTargetTimestamp
|
||||
Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
|
||||
) 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(
|
||||
long targetSentTimestamp,
|
||||
Set<RecipientIdentifier> recipients
|
||||
long targetSentTimestamp, Set<RecipientIdentifier> recipients
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
||||
|
||||
SendMessageResults sendMessageReaction(
|
||||
@ -248,102 +156,36 @@ public interface Manager extends Closeable {
|
||||
RecipientIdentifier.Single targetAuthor,
|
||||
long targetSentTimestamp,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
final boolean notifySelf,
|
||||
final boolean isStory
|
||||
) 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(
|
||||
byte[] receipt,
|
||||
String note,
|
||||
RecipientIdentifier.Single recipient
|
||||
byte[] receipt, String note, RecipientIdentifier.Single recipient
|
||||
) throws IOException;
|
||||
|
||||
void 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);
|
||||
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
||||
|
||||
void deleteRecipient(RecipientIdentifier.Single recipient);
|
||||
|
||||
void deleteContact(RecipientIdentifier.Single recipient);
|
||||
|
||||
void setContactName(
|
||||
final RecipientIdentifier.Single recipient,
|
||||
final String givenName,
|
||||
final String familyName,
|
||||
final String nickGivenName,
|
||||
final String nickFamilyName,
|
||||
final String note
|
||||
) throws UnregisteredRecipientException;
|
||||
RecipientIdentifier.Single recipient, String givenName, final String familyName
|
||||
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
|
||||
|
||||
void setContactsBlocked(
|
||||
Collection<RecipientIdentifier.Single> recipient,
|
||||
boolean blocked
|
||||
Collection<RecipientIdentifier.Single> recipient, boolean blocked
|
||||
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
|
||||
|
||||
void setGroupsBlocked(
|
||||
Collection<GroupId> groupId,
|
||||
boolean blocked
|
||||
Collection<GroupId> groupId, boolean blocked
|
||||
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
|
||||
|
||||
/**
|
||||
* Change the expiration timer for a contact
|
||||
*/
|
||||
void setExpirationTimer(
|
||||
RecipientIdentifier.Single recipient,
|
||||
int messageExpirationTimer
|
||||
RecipientIdentifier.Single recipient, int messageExpirationTimer
|
||||
) throws IOException, UnregisteredRecipientException;
|
||||
|
||||
/**
|
||||
@ -354,8 +196,6 @@ public interface Manager extends Closeable {
|
||||
*/
|
||||
StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException;
|
||||
|
||||
void installStickerPack(StickerPackUrl url) throws IOException;
|
||||
|
||||
List<StickerPack> getStickerPacks();
|
||||
|
||||
void requestAllSyncData() throws IOException;
|
||||
@ -381,16 +221,14 @@ public interface Manager extends Closeable {
|
||||
/**
|
||||
* Receive new messages from server, returns if no new message arrive in a timespan of timeout.
|
||||
*/
|
||||
void receiveMessages(
|
||||
Optional<Duration> timeout,
|
||||
Optional<Integer> maxMessages,
|
||||
ReceiveMessageHandler handler
|
||||
public void receiveMessages(
|
||||
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
|
||||
) throws IOException, AlreadyReceivingException;
|
||||
|
||||
void stopReceiveMessages();
|
||||
|
||||
void setReceiveConfig(ReceiveConfig receiveConfig);
|
||||
|
||||
boolean hasCaughtUpWithOldMessages();
|
||||
|
||||
boolean isContactBlocked(RecipientIdentifier.Single recipient);
|
||||
|
||||
void sendContacts() throws IOException;
|
||||
@ -411,13 +249,33 @@ public interface Manager extends Closeable {
|
||||
List<Identity> getIdentities(RecipientIdentifier.Single recipient);
|
||||
|
||||
/**
|
||||
* Trust this the identity with this fingerprint/safetyNumber
|
||||
* Trust this the identity with this fingerprint
|
||||
*
|
||||
* @param recipient account of the identity
|
||||
* @param recipient account of the identity
|
||||
* @param fingerprint Fingerprint
|
||||
*/
|
||||
boolean trustIdentityVerified(
|
||||
RecipientIdentifier.Single recipient,
|
||||
IdentityVerificationCode verificationCode
|
||||
RecipientIdentifier.Single recipient, byte[] fingerprint
|
||||
) throws UnregisteredRecipientException;
|
||||
|
||||
/**
|
||||
* Trust this the identity with this safety number
|
||||
*
|
||||
* @param recipient account of the identity
|
||||
* @param safetyNumber Safety number
|
||||
*/
|
||||
boolean trustIdentityVerifiedSafetyNumber(
|
||||
RecipientIdentifier.Single recipient, String safetyNumber
|
||||
) throws UnregisteredRecipientException;
|
||||
|
||||
/**
|
||||
* Trust this the identity with this scannable safety number
|
||||
*
|
||||
* @param recipient account of the identity
|
||||
* @param safetyNumber Scannable safety number
|
||||
*/
|
||||
boolean trustIdentityVerifiedSafetyNumber(
|
||||
RecipientIdentifier.Single recipient, byte[] safetyNumber
|
||||
) throws UnregisteredRecipientException;
|
||||
|
||||
/**
|
||||
@ -433,59 +291,8 @@ public interface Manager extends Closeable {
|
||||
|
||||
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
|
||||
void close();
|
||||
|
||||
void addCallEventListener(CallEventListener listener);
|
||||
|
||||
void removeCallEventListener(CallEventListener listener);
|
||||
void close() throws IOException;
|
||||
|
||||
interface ReceiveMessageHandler {
|
||||
|
||||
@ -494,9 +301,4 @@ public interface Manager extends Closeable {
|
||||
|
||||
void handleMessage(MessageEnvelope envelope, Throwable e);
|
||||
}
|
||||
|
||||
interface CallEventListener {
|
||||
|
||||
void handleCallEvent(CallInfo callInfo, String reason);
|
||||
}
|
||||
}
|
||||
|
||||
1257
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
Normal file
1257
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,8 @@
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.internal.LibSignalLogger;
|
||||
import org.asamk.signal.manager.internal.SignalLogger;
|
||||
|
||||
public class ManagerLogger {
|
||||
|
||||
public static void initLogger() {
|
||||
LibSignalLogger.initLogger();
|
||||
SignalLogger.initLogger();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
package org.asamk.signal.manager.internal;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.MultiAccountManager;
|
||||
import org.asamk.signal.manager.ProvisioningManager;
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
import org.asamk.signal.manager.SignalAccountFiles;
|
||||
import org.asamk.signal.manager.api.AccountCheckException;
|
||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
||||
import org.signal.core.util.UuidUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -23,9 +15,9 @@ import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MultiAccountManagerImpl.class);
|
||||
private final static Logger logger = LoggerFactory.getLogger(MultiAccountManagerImpl.class);
|
||||
|
||||
private final Set<Consumer<Manager>> onManagerAddedHandlers = new HashSet<>();
|
||||
private final Set<Consumer<Manager>> onManagerRemovedHandlers = new HashSet<>();
|
||||
@ -53,7 +45,7 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void addManager(final Manager m) {
|
||||
void addManager(final Manager m) {
|
||||
synchronized (managers) {
|
||||
if (managers.contains(m)) {
|
||||
return;
|
||||
@ -96,47 +88,9 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manager getManager(final String identifier) {
|
||||
public Manager getManager(final String number) {
|
||||
synchronized (managers) {
|
||||
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
||||
// Check if UUID corresponds to an already-loaded manager
|
||||
final var existing = managers.stream()
|
||||
.filter(m -> m.getSelfACI().equals(identifier))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (existing != null) {
|
||||
logger.debug("Found already loaded manager for ACI: {}", identifier);
|
||||
return existing;
|
||||
}
|
||||
// Load by ACI
|
||||
try {
|
||||
final var newManager = signalAccountFiles.initManagerByAci(identifier);
|
||||
managers.add(newManager);
|
||||
return newManager;
|
||||
} catch (NotRegisteredException e) {
|
||||
logger.debug("Manager not found by ACI: {}", identifier);
|
||||
} catch (IOException | IllegalArgumentException | AccountCheckException e) {
|
||||
logger.warn("Failed to load new manager by ACI: {}", identifier, e);
|
||||
}
|
||||
} else {
|
||||
// Phone number — check already loaded managers
|
||||
var existing = managers.stream()
|
||||
.filter(m -> m.getSelfNumber().equals(identifier))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
// Load by phone number
|
||||
try {
|
||||
final var newManager = signalAccountFiles.initManagerByNumber(identifier);
|
||||
managers.add(newManager);
|
||||
return newManager;
|
||||
} catch (NotRegisteredException | IOException | IllegalArgumentException | AccountCheckException e) {
|
||||
logger.warn("Failed to load manager by number: {}", identifier, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return managers.stream().filter(m -> m.getSelfNumber().equals(number)).findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,20 +118,14 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
final List<Thread> closeThreads;
|
||||
synchronized (managers) {
|
||||
closeThreads = new ArrayList<>(managers).stream()
|
||||
.map(m -> Thread.ofPlatform().name("manager-close-" + m.getSelfNumber()).start(m::close))
|
||||
.toList();
|
||||
}
|
||||
|
||||
for (final var t : closeThreads) {
|
||||
try {
|
||||
t.join();
|
||||
} catch (InterruptedException ignored) {
|
||||
for (var m : new ArrayList<>(managers)) {
|
||||
try {
|
||||
m.close();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Cleanup failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
synchronized (managers) {
|
||||
managers.clear();
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
package org.asamk.signal.manager.internal;
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public record PathConfig(
|
||||
record PathConfig(
|
||||
File dataPath, File attachmentsPath, File avatarsPath, File stickerPacksPath
|
||||
) {
|
||||
|
||||
public static PathConfig createDefault(final File settingsPath) {
|
||||
static PathConfig createDefault(final File settingsPath) {
|
||||
return new PathConfig(new File(settingsPath, "data"),
|
||||
new File(settingsPath, "attachments"),
|
||||
new File(settingsPath, "avatars"),
|
||||
@ -0,0 +1,241 @@
|
||||
/*
|
||||
Copyright (C) 2015-2022 AsamK and contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.api.UserAlreadyExistsException;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.util.KeyHelper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||
import org.whispersystems.signalservice.internal.push.ConfirmCodeMessage;
|
||||
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.getCapabilities;
|
||||
|
||||
class ProvisioningManagerImpl implements ProvisioningManager {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
|
||||
|
||||
private final PathConfig pathConfig;
|
||||
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
|
||||
private final String userAgent;
|
||||
private final Consumer<Manager> newManagerListener;
|
||||
private final AccountsStore accountsStore;
|
||||
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
private final IdentityKeyPair tempIdentityKey;
|
||||
private final int registrationId;
|
||||
private final int pniRegistrationId;
|
||||
private final String password;
|
||||
|
||||
ProvisioningManagerImpl(
|
||||
PathConfig pathConfig,
|
||||
ServiceEnvironmentConfig serviceEnvironmentConfig,
|
||||
String userAgent,
|
||||
final Consumer<Manager> newManagerListener,
|
||||
final AccountsStore accountsStore
|
||||
) {
|
||||
this.pathConfig = pathConfig;
|
||||
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
|
||||
this.userAgent = userAgent;
|
||||
this.newManagerListener = newManagerListener;
|
||||
this.accountsStore = accountsStore;
|
||||
|
||||
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||
registrationId = KeyHelper.generateRegistrationId(false);
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false);
|
||||
password = KeyUtils.createPassword();
|
||||
GroupsV2Operations groupsV2Operations;
|
||||
try {
|
||||
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()),
|
||||
ServiceConfig.GROUP_MAX_SIZE);
|
||||
} catch (Throwable ignored) {
|
||||
groupsV2Operations = null;
|
||||
}
|
||||
accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
new DynamicCredentialsProvider(null, null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID),
|
||||
userAgent,
|
||||
groupsV2Operations,
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
||||
var deviceUuid = accountManager.getNewDeviceUuid();
|
||||
|
||||
return new DeviceLinkInfo(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
||||
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
|
||||
var number = ret.getNumber();
|
||||
var aci = ret.getAci();
|
||||
var pni = ret.getPni();
|
||||
|
||||
logger.info("Received link information from {}, linking in progress ...", number);
|
||||
|
||||
var accountPath = accountsStore.getPathByAci(aci);
|
||||
if (accountPath == null) {
|
||||
accountPath = accountsStore.getPathByNumber(number);
|
||||
}
|
||||
if (accountPath != null
|
||||
&& SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)
|
||||
&& !canRelinkExistingAccount(accountPath)) {
|
||||
throw new UserAlreadyExistsException(number, SignalAccount.getFileName(pathConfig.dataPath(), accountPath));
|
||||
}
|
||||
if (accountPath == null) {
|
||||
accountPath = accountsStore.addAccount(number, aci);
|
||||
} else {
|
||||
accountsStore.updateAccount(accountPath, number, aci);
|
||||
}
|
||||
|
||||
var encryptedDeviceName = deviceName == null
|
||||
? null
|
||||
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getAciIdentity().getPrivateKey());
|
||||
|
||||
logger.debug("Finishing new device registration");
|
||||
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
||||
new ConfirmCodeMessage(false,
|
||||
true,
|
||||
registrationId,
|
||||
pniRegistrationId,
|
||||
encryptedDeviceName,
|
||||
getCapabilities(false)));
|
||||
|
||||
// Create new account with the synced identity
|
||||
var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
|
||||
|
||||
SignalAccount account = null;
|
||||
try {
|
||||
account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.dataPath(),
|
||||
accountPath,
|
||||
number,
|
||||
serviceEnvironmentConfig.getType(),
|
||||
aci,
|
||||
pni,
|
||||
password,
|
||||
encryptedDeviceName,
|
||||
deviceId,
|
||||
ret.getAciIdentity(),
|
||||
ret.getPniIdentity(),
|
||||
registrationId,
|
||||
pniRegistrationId,
|
||||
profileKey,
|
||||
Settings.DEFAULT);
|
||||
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
|
||||
|
||||
ManagerImpl m = null;
|
||||
try {
|
||||
m = new ManagerImpl(account,
|
||||
pathConfig,
|
||||
new AccountFileUpdaterImpl(accountsStore, accountPath),
|
||||
serviceEnvironmentConfig,
|
||||
userAgent);
|
||||
account = null;
|
||||
|
||||
logger.debug("Refreshing pre keys");
|
||||
try {
|
||||
m.refreshPreKeys();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to refresh pre keys.", e);
|
||||
}
|
||||
|
||||
logger.debug("Requesting sync data");
|
||||
try {
|
||||
m.requestAllSyncData();
|
||||
} catch (Exception e) {
|
||||
logger.error(
|
||||
"Failed to request sync messages from linked device, data can be requested again with `sendSyncRequest`.",
|
||||
e);
|
||||
}
|
||||
|
||||
if (newManagerListener != null) {
|
||||
newManagerListener.accept(m);
|
||||
m = null;
|
||||
}
|
||||
return number;
|
||||
} finally {
|
||||
if (m != null) {
|
||||
m.close();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (account != null) {
|
||||
account.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
|
||||
final SignalAccount signalAccount;
|
||||
try {
|
||||
signalAccount = SignalAccount.load(pathConfig.dataPath(), accountPath, false, Settings.DEFAULT);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Account in use or failed to load.", e);
|
||||
return false;
|
||||
} catch (OverlappingFileLockException e) {
|
||||
logger.debug("Account in use.", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try (signalAccount) {
|
||||
if (signalAccount.isPrimaryDevice()) {
|
||||
logger.debug("Account is a primary device.");
|
||||
return false;
|
||||
}
|
||||
if (signalAccount.isRegistered()
|
||||
&& signalAccount.getServiceEnvironment() != null
|
||||
&& signalAccount.getServiceEnvironment() != serviceEnvironmentConfig.getType()) {
|
||||
logger.debug("Account is registered in another environment: {}.",
|
||||
signalAccount.getServiceEnvironment());
|
||||
return false;
|
||||
}
|
||||
|
||||
final var m = new ManagerImpl(signalAccount,
|
||||
pathConfig,
|
||||
new AccountFileUpdaterImpl(accountsStore, accountPath),
|
||||
serviceEnvironmentConfig,
|
||||
userAgent);
|
||||
try (m) {
|
||||
m.checkAccountState();
|
||||
} catch (AuthorizationFailedException ignored) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug("Account is still successfully linked.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,10 +3,8 @@ package org.asamk.signal.manager;
|
||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
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.RateLimitException;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
@ -14,15 +12,12 @@ import java.io.IOException;
|
||||
public interface RegistrationManager extends Closeable {
|
||||
|
||||
void register(
|
||||
boolean voiceVerification,
|
||||
String captcha,
|
||||
final boolean forceRegister
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
|
||||
boolean voiceVerification, String captcha
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException;
|
||||
|
||||
void verifyAccount(
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
|
||||
String verificationCode, String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException;
|
||||
|
||||
void deleteLocalAccountData() throws IOException;
|
||||
|
||||
|
||||
@ -0,0 +1,243 @@
|
||||
/*
|
||||
Copyright (C) 2015-2022 AsamK and contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.api.UpdateProfile;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.asamk.signal.manager.helper.AccountFileUpdater;
|
||||
import org.asamk.signal.manager.helper.PinHelper;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
class RegistrationManagerImpl implements RegistrationManager {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class);
|
||||
|
||||
private SignalAccount account;
|
||||
private final PathConfig pathConfig;
|
||||
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
|
||||
private final String userAgent;
|
||||
private final Consumer<Manager> newManagerListener;
|
||||
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
private final PinHelper pinHelper;
|
||||
private final AccountFileUpdater accountFileUpdater;
|
||||
|
||||
RegistrationManagerImpl(
|
||||
SignalAccount account,
|
||||
PathConfig pathConfig,
|
||||
ServiceEnvironmentConfig serviceEnvironmentConfig,
|
||||
String userAgent,
|
||||
Consumer<Manager> newManagerListener,
|
||||
AccountFileUpdater accountFileUpdater
|
||||
) {
|
||||
this.account = account;
|
||||
this.pathConfig = pathConfig;
|
||||
this.accountFileUpdater = accountFileUpdater;
|
||||
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
|
||||
this.userAgent = userAgent;
|
||||
this.newManagerListener = newManagerListener;
|
||||
|
||||
GroupsV2Operations groupsV2Operations;
|
||||
try {
|
||||
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()),
|
||||
ServiceConfig.GROUP_MAX_SIZE);
|
||||
} catch (Throwable ignored) {
|
||||
groupsV2Operations = null;
|
||||
}
|
||||
this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
new DynamicCredentialsProvider(
|
||||
// Using empty UUID, because registering doesn't work otherwise
|
||||
null, null, account.getNumber(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID),
|
||||
userAgent,
|
||||
groupsV2Operations,
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||
final var keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
|
||||
serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
|
||||
serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
|
||||
serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
|
||||
10);
|
||||
final var fallbackKeyBackupServices = serviceEnvironmentConfig.getFallbackKeyBackupConfigs()
|
||||
.stream()
|
||||
.map(config -> accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
|
||||
config.getEnclaveName(),
|
||||
config.getServiceId(),
|
||||
config.getMrenclave(),
|
||||
10))
|
||||
.toList();
|
||||
this.pinHelper = new PinHelper(keyBackupService, fallbackKeyBackupServices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(
|
||||
boolean voiceVerification, String captcha
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
|
||||
if (account.isRegistered()
|
||||
&& account.getServiceEnvironment() != null
|
||||
&& account.getServiceEnvironment() != serviceEnvironmentConfig.getType()) {
|
||||
throw new IOException("Account is registered in another environment: " + account.getServiceEnvironment());
|
||||
}
|
||||
|
||||
if (account.getAci() != null && attemptReactivateAccount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
|
||||
account.getSessionId(account.getNumber()),
|
||||
id -> account.setSessionId(account.getNumber(), id),
|
||||
voiceVerification,
|
||||
captcha);
|
||||
NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyAccount(
|
||||
String verificationCode, String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException {
|
||||
var sessionId = account.getSessionId(account.getNumber());
|
||||
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
||||
verificationCode,
|
||||
pin,
|
||||
pinHelper,
|
||||
this::verifyAccountWithCode);
|
||||
final var response = result.first();
|
||||
final var masterKey = result.second();
|
||||
if (masterKey == null) {
|
||||
pin = null;
|
||||
}
|
||||
|
||||
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
|
||||
final var aci = ACI.parseOrNull(response.getUuid());
|
||||
final var pni = PNI.parseOrNull(response.getPni());
|
||||
account.finishRegistration(aci, pni, masterKey, pin);
|
||||
accountFileUpdater.updateAccountIdentifiers(account.getNumber(), aci);
|
||||
|
||||
ManagerImpl m = null;
|
||||
try {
|
||||
m = new ManagerImpl(account, pathConfig, accountFileUpdater, serviceEnvironmentConfig, userAgent);
|
||||
account = null;
|
||||
|
||||
m.refreshPreKeys();
|
||||
if (response.isStorageCapable()) {
|
||||
m.retrieveRemoteStorage();
|
||||
}
|
||||
// Set an initial empty profile so user can be added to groups
|
||||
try {
|
||||
m.updateProfile(UpdateProfile.newBuilder().build());
|
||||
} catch (NoClassDefFoundError e) {
|
||||
logger.warn("Failed to set default profile: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
m.refreshCurrentUsername();
|
||||
} catch (IOException | BaseUsernameException e) {
|
||||
logger.warn("Failed to refresh current username", e);
|
||||
}
|
||||
|
||||
if (newManagerListener != null) {
|
||||
newManagerListener.accept(m);
|
||||
m = null;
|
||||
}
|
||||
} finally {
|
||||
if (m != null) {
|
||||
m.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteLocalAccountData() throws IOException {
|
||||
account.deleteAccountData();
|
||||
accountFileUpdater.removeAccount();
|
||||
account = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered() {
|
||||
return account.isRegistered();
|
||||
}
|
||||
|
||||
private boolean attemptReactivateAccount() {
|
||||
try {
|
||||
final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
account.getCredentialsProvider(),
|
||||
userAgent,
|
||||
null,
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||
accountManager.setAccountAttributes(account.getAccountAttributes(null));
|
||||
account.setRegistered(true);
|
||||
logger.info("Reactivated existing account, verify is not necessary.");
|
||||
if (newManagerListener != null) {
|
||||
final var m = new ManagerImpl(account,
|
||||
pathConfig,
|
||||
accountFileUpdater,
|
||||
serviceEnvironmentConfig,
|
||||
userAgent);
|
||||
account = null;
|
||||
newManagerListener.accept(m);
|
||||
}
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
logger.debug("Failed to reactivate account");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private VerifyAccountResponse verifyAccountWithCode(
|
||||
final String sessionId, final String verificationCode, final String registrationLock
|
||||
) throws IOException {
|
||||
try {
|
||||
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId));
|
||||
} catch (AlreadyVerifiedException e) {
|
||||
// Already verified so can continue registering
|
||||
}
|
||||
return Utils.handleResponseException(accountManager.registerAccount(sessionId,
|
||||
null,
|
||||
account.getAccountAttributes(registrationLock),
|
||||
true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (account != null) {
|
||||
account.close();
|
||||
account = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.api.TrustNewIdentity;
|
||||
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -2,20 +2,13 @@ package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.api.AccountCheckException;
|
||||
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.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironment;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.asamk.signal.manager.internal.AccountFileUpdaterImpl;
|
||||
import org.asamk.signal.manager.internal.ManagerImpl;
|
||||
import org.asamk.signal.manager.internal.MultiAccountManagerImpl;
|
||||
import org.asamk.signal.manager.internal.PathConfig;
|
||||
import org.asamk.signal.manager.internal.ProvisioningManagerImpl;
|
||||
import org.asamk.signal.manager.internal.RegistrationManagerImpl;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||
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.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||
@ -66,56 +59,29 @@ public class SignalAccountFiles {
|
||||
}
|
||||
|
||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||
try {
|
||||
return new Pair<Manager, Throwable>(initManagerByNumber(a.number(), a.path()), null);
|
||||
} catch (NotRegisteredException e) {
|
||||
return initManager(a.number(), a.path());
|
||||
} catch (NotRegisteredException | IOException | AccountCheckException e) {
|
||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||
return null;
|
||||
} catch (AccountCheckException | IOException e) {
|
||||
} catch (Throwable e) {
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
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 {
|
||||
final var aci = ACI.parseOrThrow(aciStr);
|
||||
final var accountPath = accountsStore.getPathByAci(aci);
|
||||
return this.initManagerByAci(aci, accountPath);
|
||||
}
|
||||
|
||||
private Manager initManagerByNumber(
|
||||
String number,
|
||||
String accountPath
|
||||
private Manager initManager(
|
||||
String number, String accountPath
|
||||
) 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) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
@ -123,27 +89,12 @@ public class SignalAccountFiles {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
|
||||
return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||
}
|
||||
|
||||
private Manager initManagerByAci(
|
||||
ACI aci,
|
||||
String accountPath
|
||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var account = loadAccount(accountPath);
|
||||
if (!aci.equals(account.getAci())) {
|
||||
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||
if (!number.equals(account.getNumber())) {
|
||||
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()) {
|
||||
account.close();
|
||||
throw new NotRegisteredException();
|
||||
@ -165,10 +116,10 @@ public class SignalAccountFiles {
|
||||
manager.checkAccountState();
|
||||
} catch (DeprecatedVersionException e) {
|
||||
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) {
|
||||
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) {
|
||||
@ -196,14 +147,15 @@ public class SignalAccountFiles {
|
||||
}
|
||||
|
||||
public RegistrationManager initRegistrationManager(
|
||||
String number,
|
||||
Consumer<Manager> newManagerListener
|
||||
String number, Consumer<Manager> newManagerListener
|
||||
) throws IOException {
|
||||
final var accountPath = accountsStore.getPathByNumber(number);
|
||||
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
||||
final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
|
||||
var aciIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||
var pniIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||
var registrationId = KeyHelper.generateRegistrationId(false);
|
||||
var pniRegistrationId = KeyHelper.generateRegistrationId(false);
|
||||
|
||||
var profileKey = KeyUtils.createProfileKey();
|
||||
var account = SignalAccount.create(pathConfig.dataPath(),
|
||||
@ -212,9 +164,10 @@ public class SignalAccountFiles {
|
||||
serviceEnvironment,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
registrationId,
|
||||
pniRegistrationId,
|
||||
profileKey,
|
||||
settings);
|
||||
account.initDatabase();
|
||||
|
||||
return new RegistrationManagerImpl(account,
|
||||
pathConfig,
|
||||
@ -229,7 +182,6 @@ public class SignalAccountFiles {
|
||||
account.close();
|
||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||
}
|
||||
account.initDatabase();
|
||||
|
||||
return new RegistrationManagerImpl(account,
|
||||
pathConfig,
|
||||
|
||||
@ -0,0 +1,244 @@
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SignalDependencies {
|
||||
|
||||
private final Object LOCK = new Object();
|
||||
|
||||
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
|
||||
private final String userAgent;
|
||||
private final CredentialsProvider credentialsProvider;
|
||||
private final SignalServiceDataStore dataStore;
|
||||
private final ExecutorService executor;
|
||||
private final SignalSessionLock sessionLock;
|
||||
|
||||
private boolean allowStories = true;
|
||||
|
||||
private SignalServiceAccountManager accountManager;
|
||||
private GroupsV2Api groupsV2Api;
|
||||
private GroupsV2Operations groupsV2Operations;
|
||||
private ClientZkOperations clientZkOperations;
|
||||
|
||||
private SignalWebSocket signalWebSocket;
|
||||
private SignalServiceMessageReceiver messageReceiver;
|
||||
private SignalServiceMessageSender messageSender;
|
||||
|
||||
private KeyBackupService keyBackupService;
|
||||
private ProfileService profileService;
|
||||
private SignalServiceCipher cipher;
|
||||
|
||||
SignalDependencies(
|
||||
final ServiceEnvironmentConfig serviceEnvironmentConfig,
|
||||
final String userAgent,
|
||||
final CredentialsProvider credentialsProvider,
|
||||
final SignalServiceDataStore dataStore,
|
||||
final ExecutorService executor,
|
||||
final SignalSessionLock sessionLock
|
||||
) {
|
||||
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
|
||||
this.userAgent = userAgent;
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
this.dataStore = dataStore;
|
||||
this.executor = executor;
|
||||
this.sessionLock = sessionLock;
|
||||
}
|
||||
|
||||
public void resetAfterAddressChange() {
|
||||
this.messageSender = null;
|
||||
this.cipher = null;
|
||||
getSignalWebSocket().forceNewWebSockets();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method needs to be called before the first websocket is created
|
||||
*/
|
||||
public void setAllowStories(final boolean allowStories) {
|
||||
this.allowStories = allowStories;
|
||||
}
|
||||
|
||||
public ServiceEnvironmentConfig getServiceEnvironmentConfig() {
|
||||
return serviceEnvironmentConfig;
|
||||
}
|
||||
|
||||
public SignalSessionLock getSessionLock() {
|
||||
return sessionLock;
|
||||
}
|
||||
|
||||
public SignalServiceAccountManager getAccountManager() {
|
||||
return getOrCreate(() -> accountManager,
|
||||
() -> accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
credentialsProvider,
|
||||
userAgent,
|
||||
getGroupsV2Operations(),
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
|
||||
}
|
||||
|
||||
public SignalServiceAccountManager createUnauthenticatedAccountManager(String number, String password) {
|
||||
return new SignalServiceAccountManager(getServiceEnvironmentConfig().getSignalServiceConfiguration(),
|
||||
null,
|
||||
null,
|
||||
number,
|
||||
SignalServiceAddress.DEFAULT_DEVICE_ID,
|
||||
password,
|
||||
userAgent,
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY,
|
||||
ServiceConfig.GROUP_MAX_SIZE);
|
||||
}
|
||||
|
||||
public GroupsV2Api getGroupsV2Api() {
|
||||
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
|
||||
}
|
||||
|
||||
public GroupsV2Operations getGroupsV2Operations() {
|
||||
return getOrCreate(() -> groupsV2Operations,
|
||||
() -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()),
|
||||
ServiceConfig.GROUP_MAX_SIZE));
|
||||
}
|
||||
|
||||
private ClientZkOperations getClientZkOperations() {
|
||||
return getOrCreate(() -> clientZkOperations,
|
||||
() -> clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
|
||||
}
|
||||
|
||||
private ClientZkProfileOperations getClientZkProfileOperations() {
|
||||
final var clientZkOperations = getClientZkOperations();
|
||||
return clientZkOperations == null ? null : clientZkOperations.getProfileOperations();
|
||||
}
|
||||
|
||||
public SignalWebSocket getSignalWebSocket() {
|
||||
return getOrCreate(() -> signalWebSocket, () -> {
|
||||
final var timer = new UptimeSleepTimer();
|
||||
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
|
||||
final var webSocketFactory = new WebSocketFactory() {
|
||||
@Override
|
||||
public WebSocketConnection createWebSocket() {
|
||||
return new WebSocketConnection("normal",
|
||||
serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
Optional.of(credentialsProvider),
|
||||
userAgent,
|
||||
healthMonitor,
|
||||
allowStories);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketConnection createUnidentifiedWebSocket() {
|
||||
return new WebSocketConnection("unidentified",
|
||||
serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
Optional.empty(),
|
||||
userAgent,
|
||||
healthMonitor,
|
||||
allowStories);
|
||||
}
|
||||
};
|
||||
signalWebSocket = new SignalWebSocket(webSocketFactory);
|
||||
healthMonitor.monitor(signalWebSocket);
|
||||
});
|
||||
}
|
||||
|
||||
public SignalServiceMessageReceiver getMessageReceiver() {
|
||||
return getOrCreate(() -> messageReceiver,
|
||||
() -> messageReceiver = new SignalServiceMessageReceiver(serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
credentialsProvider,
|
||||
userAgent,
|
||||
getClientZkProfileOperations(),
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
|
||||
}
|
||||
|
||||
public SignalServiceMessageSender getMessageSender() {
|
||||
return getOrCreate(() -> messageSender,
|
||||
() -> messageSender = new SignalServiceMessageSender(serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
credentialsProvider,
|
||||
dataStore,
|
||||
sessionLock,
|
||||
userAgent,
|
||||
getSignalWebSocket(),
|
||||
Optional.empty(),
|
||||
getClientZkProfileOperations(),
|
||||
executor,
|
||||
ServiceConfig.MAX_ENVELOPE_SIZE,
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
|
||||
}
|
||||
|
||||
public KeyBackupService getKeyBackupService() {
|
||||
return getOrCreate(() -> keyBackupService,
|
||||
() -> keyBackupService = getAccountManager().getKeyBackupService(ServiceConfig.getIasKeyStore(),
|
||||
serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
|
||||
serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
|
||||
serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
|
||||
10));
|
||||
}
|
||||
|
||||
public Collection<KeyBackupService> getFallbackKeyBackupServices() {
|
||||
return serviceEnvironmentConfig.getFallbackKeyBackupConfigs()
|
||||
.stream()
|
||||
.map(config -> getAccountManager().getKeyBackupService(ServiceConfig.getIasKeyStore(),
|
||||
config.getEnclaveName(),
|
||||
config.getServiceId(),
|
||||
config.getMrenclave(),
|
||||
10))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public ProfileService getProfileService() {
|
||||
return getOrCreate(() -> profileService,
|
||||
() -> profileService = new ProfileService(getClientZkProfileOperations(),
|
||||
getMessageReceiver(),
|
||||
getSignalWebSocket()));
|
||||
}
|
||||
|
||||
public SignalServiceCipher getCipher() {
|
||||
return getOrCreate(() -> cipher, () -> {
|
||||
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.getUnidentifiedSenderTrustRoot());
|
||||
final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164());
|
||||
final var deviceId = credentialsProvider.getDeviceId();
|
||||
cipher = new SignalServiceCipher(address, deviceId, dataStore.aci(), sessionLock, certificateValidator);
|
||||
});
|
||||
}
|
||||
|
||||
private <T> T getOrCreate(Supplier<T> supplier, Callable creator) {
|
||||
var value = supplier.get();
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
synchronized (LOCK) {
|
||||
value = supplier.get();
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
creator.call();
|
||||
return supplier.get();
|
||||
}
|
||||
}
|
||||
|
||||
private interface Callable {
|
||||
|
||||
void call();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Monitors the health of the identified and unidentified WebSockets. If either one appears to be
|
||||
* unhealthy, will trigger restarting both.
|
||||
* <p>
|
||||
* The monitor is also responsible for sending heartbeats/keep-alive messages to prevent
|
||||
* timeouts.
|
||||
*/
|
||||
final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class);
|
||||
|
||||
private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(WebSocketConnection.KEEPALIVE_TIMEOUT_SECONDS);
|
||||
private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3;
|
||||
|
||||
private SignalWebSocket signalWebSocket;
|
||||
private final SleepTimer sleepTimer;
|
||||
|
||||
private volatile KeepAliveSender keepAliveSender;
|
||||
|
||||
private final HealthState identified = new HealthState();
|
||||
private final HealthState unidentified = new HealthState();
|
||||
|
||||
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
|
||||
this.sleepTimer = sleepTimer;
|
||||
}
|
||||
|
||||
public void monitor(SignalWebSocket signalWebSocket) {
|
||||
Preconditions.checkNotNull(signalWebSocket);
|
||||
Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once");
|
||||
|
||||
this.signalWebSocket = signalWebSocket;
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
signalWebSocket.getWebSocketState()
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(Schedulers.computation())
|
||||
.distinctUntilChanged()
|
||||
.subscribe(s -> onStateChange(s, identified));
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
signalWebSocket.getUnidentifiedWebSocketState()
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(Schedulers.computation())
|
||||
.distinctUntilChanged()
|
||||
.subscribe(s -> onStateChange(s, unidentified));
|
||||
}
|
||||
|
||||
private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) {
|
||||
switch (connectionState) {
|
||||
case CONNECTED -> logger.debug("WebSocket is now connected");
|
||||
case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed");
|
||||
case FAILED -> logger.debug("WebSocket connection failed");
|
||||
}
|
||||
|
||||
healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
|
||||
|
||||
if (keepAliveSender == null && isKeepAliveNecessary()) {
|
||||
keepAliveSender = new KeepAliveSender();
|
||||
keepAliveSender.start();
|
||||
} else if (keepAliveSender != null && !isKeepAliveNecessary()) {
|
||||
keepAliveSender.shutdown();
|
||||
keepAliveSender = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
|
||||
if (isIdentifiedWebSocket) {
|
||||
identified.lastKeepAliveReceived = System.currentTimeMillis();
|
||||
} else {
|
||||
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
|
||||
if (status == 409) {
|
||||
HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified);
|
||||
if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
|
||||
logger.warn("Received too many mismatch device errors, forcing new websockets.");
|
||||
signalWebSocket.forceNewWebSockets();
|
||||
signalWebSocket.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isKeepAliveNecessary() {
|
||||
return identified.needsKeepAlive || unidentified.needsKeepAlive;
|
||||
}
|
||||
|
||||
private static class HealthState {
|
||||
|
||||
private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1));
|
||||
|
||||
private volatile boolean needsKeepAlive;
|
||||
private volatile long lastKeepAliveReceived;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If
|
||||
* either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated.
|
||||
*/
|
||||
private class KeepAliveSender extends Thread {
|
||||
|
||||
private volatile boolean shouldKeepRunning = true;
|
||||
|
||||
public void run() {
|
||||
identified.lastKeepAliveReceived = System.currentTimeMillis();
|
||||
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
|
||||
|
||||
while (shouldKeepRunning && isKeepAliveNecessary()) {
|
||||
try {
|
||||
sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE);
|
||||
|
||||
if (shouldKeepRunning && isKeepAliveNecessary()) {
|
||||
long keepAliveRequiredSinceTime = System.currentTimeMillis()
|
||||
- MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE;
|
||||
|
||||
if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime
|
||||
|| unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) {
|
||||
logger.warn("Missed keep alives, identified last: "
|
||||
+ identified.lastKeepAliveReceived
|
||||
+ " unidentified last: "
|
||||
+ unidentified.lastKeepAliveReceived
|
||||
+ " needed by: "
|
||||
+ keepAliveRequiredSinceTime);
|
||||
signalWebSocket.forceNewWebSockets();
|
||||
signalWebSocket.connect();
|
||||
} else {
|
||||
signalWebSocket.sendKeepAlive();
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.warn("Error occured in KeepAliveSender, ignoring ...", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
shouldKeepRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private final static class HttpErrorTracker {
|
||||
|
||||
private final long[] timestamps;
|
||||
private final long errorTimeRange;
|
||||
|
||||
public HttpErrorTracker(int samples, long errorTimeRange) {
|
||||
this.timestamps = new long[samples];
|
||||
this.errorTimeRange = errorTimeRange;
|
||||
}
|
||||
|
||||
public synchronized boolean addSample(long now) {
|
||||
long errorsMustBeAfter = now - errorTimeRange;
|
||||
int count = 1;
|
||||
int minIndex = 0;
|
||||
|
||||
for (int i = 0; i < timestamps.length; i++) {
|
||||
if (timestamps[i] < errorsMustBeAfter) {
|
||||
timestamps[i] = 0;
|
||||
} else if (timestamps[i] != 0) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (timestamps[i] < timestamps[minIndex]) {
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
timestamps[minIndex] = now;
|
||||
|
||||
if (count >= timestamps.length) {
|
||||
Arrays.fill(timestamps, 0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,24 +2,24 @@ package org.asamk.signal.manager.actions;
|
||||
|
||||
import org.asamk.signal.manager.helper.Context;
|
||||
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 {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final ServiceId serviceId;
|
||||
private final ServiceId accountId;
|
||||
|
||||
public RenewSessionAction(final RecipientId recipientId, final ServiceId serviceId, final ServiceId accountId) {
|
||||
public RenewSessionAction(final RecipientId recipientId, final ServiceId serviceId) {
|
||||
this.recipientId = recipientId;
|
||||
this.serviceId = serviceId;
|
||||
this.accountId = accountId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Context context) throws Throwable {
|
||||
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
|
||||
context.getSendHelper().sendNullMessage(recipientId);
|
||||
context.getAccount().getAciSessionStore().archiveSessions(serviceId);
|
||||
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) {
|
||||
context.getSendHelper().sendNullMessage(recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -13,9 +13,7 @@ public class ResendMessageAction implements HandleAction {
|
||||
private final MessageSendLogEntry messageSendLogEntry;
|
||||
|
||||
public ResendMessageAction(
|
||||
final RecipientId recipientId,
|
||||
final long timestamp,
|
||||
final MessageSendLogEntry messageSendLogEntry
|
||||
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.timestamp = timestamp;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package org.asamk.signal.manager.actions;
|
||||
|
||||
import org.asamk.signal.manager.api.GroupIdV1;
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.asamk.signal.manager.helper.Context;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package org.asamk.signal.manager.actions;
|
||||
|
||||
import org.asamk.signal.manager.api.GroupIdV1;
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.asamk.signal.manager.helper.Context;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package org.asamk.signal.manager.actions;
|
||||
|
||||
import org.asamk.signal.manager.helper.Context;
|
||||
|
||||
public class SendPniIdentityKeyAction implements HandleAction {
|
||||
|
||||
private static final SendPniIdentityKeyAction INSTANCE = new SendPniIdentityKeyAction();
|
||||
|
||||
private SendPniIdentityKeyAction() {
|
||||
}
|
||||
|
||||
public static SendPniIdentityKeyAction create() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Context context) throws Throwable {
|
||||
context.getSyncHelper().sendPniIdentity();
|
||||
}
|
||||
}
|
||||
@ -15,9 +15,7 @@ public class SendReceiptAction implements HandleAction {
|
||||
private final List<Long> timestamps = new ArrayList<>();
|
||||
|
||||
public SendReceiptAction(
|
||||
final RecipientId recipientId,
|
||||
final SignalServiceReceiptMessage.Type type,
|
||||
final long timestamp
|
||||
final RecipientId recipientId, final SignalServiceReceiptMessage.Type type, final long timestamp
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.type = type;
|
||||
|
||||
@ -1,34 +1,40 @@
|
||||
package org.asamk.signal.manager.actions;
|
||||
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.helper.Context;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.signal.libsignal.metadata.ProtocolException;
|
||||
import org.signal.libsignal.protocol.message.CiphertextMessage;
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.internal.push.Envelope;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class SendRetryMessageRequestAction implements HandleAction {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final ServiceId serviceId;
|
||||
private final ProtocolException protocolException;
|
||||
private final SignalServiceEnvelope envelope;
|
||||
|
||||
public SendRetryMessageRequestAction(
|
||||
final RecipientId recipientId,
|
||||
final ServiceId serviceId,
|
||||
final ProtocolException protocolException,
|
||||
final SignalServiceEnvelope envelope
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.serviceId = serviceId;
|
||||
this.protocolException = protocolException;
|
||||
this.envelope = envelope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Context context) throws Throwable {
|
||||
context.getAccount().getAciSessionStore().archiveSessions(serviceId);
|
||||
|
||||
int senderDevice = protocolException.getSenderDevice();
|
||||
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
|
||||
protocolException.getGroupId().get())) : Optional.empty();
|
||||
@ -41,9 +47,7 @@ public class SendRetryMessageRequestAction implements HandleAction {
|
||||
envelopeType = messageContent.getType();
|
||||
} else {
|
||||
originalContent = envelope.getContent();
|
||||
envelopeType = envelope.getType() == null
|
||||
? CiphertextMessage.WHISPER_TYPE
|
||||
: envelopeTypeToCiphertextMessageType(envelope.getType());
|
||||
envelopeType = envelopeTypeToCiphertextMessageType(envelope.getType());
|
||||
}
|
||||
|
||||
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent,
|
||||
@ -55,14 +59,10 @@ public class SendRetryMessageRequestAction implements HandleAction {
|
||||
}
|
||||
|
||||
private static int envelopeTypeToCiphertextMessageType(int envelopeType) {
|
||||
final var type = Envelope.Type.fromValue(envelopeType);
|
||||
if (type == null) {
|
||||
return CiphertextMessage.WHISPER_TYPE;
|
||||
}
|
||||
return switch (type) {
|
||||
case PREKEY_MESSAGE -> CiphertextMessage.PREKEY_TYPE;
|
||||
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
|
||||
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
|
||||
return switch (envelopeType) {
|
||||
case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE -> CiphertextMessage.PREKEY_TYPE;
|
||||
case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE -> CiphertextMessage.SENDERKEY_TYPE;
|
||||
case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
|
||||
default -> CiphertextMessage.WHISPER_TYPE;
|
||||
};
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
public record CallOffer(
|
||||
long callId, Type type, byte[] opaque
|
||||
) {
|
||||
|
||||
public enum Type {
|
||||
AUDIO,
|
||||
VIDEO
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,11 @@ package org.asamk.signal.manager.api;
|
||||
|
||||
public class CaptchaRequiredException extends Exception {
|
||||
|
||||
private long nextVerificationAttemptMilliseconds;
|
||||
private long nextAttemptTimestamp;
|
||||
|
||||
public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) {
|
||||
public CaptchaRequiredException(final long nextAttemptTimestamp) {
|
||||
super("Captcha required");
|
||||
this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds;
|
||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
||||
}
|
||||
|
||||
public CaptchaRequiredException(final String message) {
|
||||
@ -17,7 +17,7 @@ public class CaptchaRequiredException extends Exception {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public long getNextVerificationAttemptMilliseconds() {
|
||||
return nextVerificationAttemptMilliseconds;
|
||||
public long getNextAttemptTimestamp() {
|
||||
return nextAttemptTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,222 +0,0 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
public record Contact(
|
||||
String givenName,
|
||||
String familyName,
|
||||
String nickName,
|
||||
String nickNameGivenName,
|
||||
String nickNameFamilyName,
|
||||
String note,
|
||||
String color,
|
||||
int messageExpirationTime,
|
||||
int messageExpirationTimeVersion,
|
||||
long muteUntil,
|
||||
boolean hideStory,
|
||||
boolean isBlocked,
|
||||
long blockedAt,
|
||||
boolean isArchived,
|
||||
boolean isProfileSharingEnabled,
|
||||
boolean isHidden,
|
||||
Long unregisteredTimestamp
|
||||
) {
|
||||
|
||||
private Contact(final Builder builder) {
|
||||
this(builder.givenName,
|
||||
builder.familyName,
|
||||
builder.nickName,
|
||||
builder.nickNameGivenName,
|
||||
builder.nickNameFamilyName,
|
||||
builder.note,
|
||||
builder.color,
|
||||
builder.messageExpirationTime,
|
||||
builder.messageExpirationTimeVersion,
|
||||
builder.muteUntil,
|
||||
builder.hideStory,
|
||||
builder.isBlocked,
|
||||
builder.blockedAt,
|
||||
builder.isArchived,
|
||||
builder.isProfileSharingEnabled,
|
||||
builder.isHidden,
|
||||
builder.unregisteredTimestamp);
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static Builder newBuilder(final Contact copy) {
|
||||
Builder builder = new Builder();
|
||||
builder.givenName = copy.givenName();
|
||||
builder.familyName = copy.familyName();
|
||||
builder.nickName = copy.nickName();
|
||||
builder.nickNameGivenName = copy.nickNameGivenName();
|
||||
builder.nickNameFamilyName = copy.nickNameFamilyName();
|
||||
builder.note = copy.note();
|
||||
builder.color = copy.color();
|
||||
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;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
final var noGivenName = Util.isEmpty(givenName);
|
||||
final var noFamilyName = Util.isEmpty(familyName);
|
||||
|
||||
if (noGivenName && noFamilyName) {
|
||||
return "";
|
||||
} else if (noGivenName) {
|
||||
return familyName;
|
||||
} else if (noFamilyName) {
|
||||
return givenName;
|
||||
}
|
||||
|
||||
return givenName + " " + familyName;
|
||||
}
|
||||
|
||||
public String getDisplayNickname() {
|
||||
final var noNickGivenName = Util.isEmpty(nickNameGivenName);
|
||||
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 static final class Builder {
|
||||
|
||||
private String givenName;
|
||||
private String familyName;
|
||||
private String nickName;
|
||||
private String nickNameGivenName;
|
||||
private String nickNameFamilyName;
|
||||
private String note;
|
||||
private String color;
|
||||
private int messageExpirationTime;
|
||||
private int messageExpirationTimeVersion = 1;
|
||||
private long muteUntil;
|
||||
private boolean hideStory;
|
||||
private boolean isBlocked;
|
||||
private long blockedAt;
|
||||
private boolean isArchived;
|
||||
private boolean isProfileSharingEnabled;
|
||||
private boolean isHidden;
|
||||
private Long unregisteredTimestamp;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public Builder withGivenName(final String val) {
|
||||
givenName = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withFamilyName(final String val) {
|
||||
familyName = val;
|
||||
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) {
|
||||
color = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMessageExpirationTime(final int val) {
|
||||
messageExpirationTime = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMessageExpirationTimeVersion(final int val) {
|
||||
messageExpirationTimeVersion = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMuteUntil(final long val) {
|
||||
muteUntil = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withHideStory(final boolean val) {
|
||||
hideStory = 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;
|
||||
}
|
||||
|
||||
public Contact build() {
|
||||
return new Contact(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.groups.GroupPermission;
|
||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
@ -12,9 +15,10 @@ public record Group(
|
||||
String title,
|
||||
String description,
|
||||
GroupInviteLinkUrl groupInviteLinkUrl,
|
||||
Set<GroupMember> members,
|
||||
Set<RecipientAddress> members,
|
||||
Set<RecipientAddress> pendingMembers,
|
||||
Set<RecipientAddress> requestingMembers,
|
||||
Set<RecipientAddress> adminMembers,
|
||||
Set<RecipientAddress> bannedMembers,
|
||||
boolean isBlocked,
|
||||
int messageExpirationTimer,
|
||||
@ -22,14 +26,11 @@ public record Group(
|
||||
GroupPermission permissionEditDetails,
|
||||
GroupPermission permissionSendMessage,
|
||||
boolean isMember,
|
||||
boolean isAdmin,
|
||||
boolean isTerminated
|
||||
boolean isAdmin
|
||||
) {
|
||||
|
||||
public static Group from(
|
||||
final GroupInfo groupInfo,
|
||||
final RecipientAddressResolver recipientStore,
|
||||
final RecipientId selfRecipientId
|
||||
final GroupInfo groupInfo, final RecipientAddressResolver recipientStore, final RecipientId selfRecipientId
|
||||
) {
|
||||
return new Group(groupInfo.getGroupId(),
|
||||
groupInfo.getTitle(),
|
||||
@ -37,7 +38,8 @@ public record Group(
|
||||
groupInfo.getGroupInviteLink(),
|
||||
groupInfo.getMembers()
|
||||
.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()),
|
||||
groupInfo.getPendingMembers()
|
||||
.stream()
|
||||
@ -49,6 +51,11 @@ public record Group(
|
||||
.map(recipientStore::resolveRecipientAddress)
|
||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
||||
.collect(Collectors.toSet()),
|
||||
groupInfo.getAdminMembers()
|
||||
.stream()
|
||||
.map(recipientStore::resolveRecipientAddress)
|
||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
||||
.collect(Collectors.toSet()),
|
||||
groupInfo.getBannedMembers()
|
||||
.stream()
|
||||
.map(recipientStore::resolveRecipientAddress)
|
||||
@ -60,7 +67,6 @@ public record Group(
|
||||
groupInfo.getPermissionEditDetails(),
|
||||
groupInfo.getPermissionSendMessage(),
|
||||
groupInfo.isMember(selfRecipientId),
|
||||
groupInfo.isAdmin(selfRecipientId),
|
||||
groupInfo.isTerminated());
|
||||
groupInfo.isAdmin(selfRecipientId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,17 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
|
||||
public record Identity(
|
||||
RecipientAddress recipient,
|
||||
byte[] fingerprint,
|
||||
IdentityKey identityKey,
|
||||
String safetyNumber,
|
||||
byte[] scannableSafetyNumber,
|
||||
TrustLevel trustLevel,
|
||||
long dateAddedTimestamp
|
||||
) {}
|
||||
) {
|
||||
|
||||
public byte[] getFingerprint() {
|
||||
return identityKey.getPublicKey().serialize();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.signal.libsignal.protocol.util.Hex;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
public sealed interface IdentityVerificationCode {
|
||||
|
||||
record Fingerprint(byte[] fingerprint) implements IdentityVerificationCode {}
|
||||
|
||||
record SafetyNumber(String safetyNumber) implements IdentityVerificationCode {}
|
||||
|
||||
record ScannableSafetyNumber(byte[] safetyNumber) implements IdentityVerificationCode {}
|
||||
|
||||
static IdentityVerificationCode parse(String code) throws Exception {
|
||||
code = code.replaceAll(" ", "");
|
||||
if (code.length() == 66) {
|
||||
final var fingerprintBytes = Hex.fromStringCondensed(code.toLowerCase(Locale.ROOT));
|
||||
return new Fingerprint(fingerprintBytes);
|
||||
} else if (code.length() == 60) {
|
||||
return new SafetyNumber(code);
|
||||
} else {
|
||||
final var scannableSafetyNumber = Base64.getDecoder().decode(code);
|
||||
return new ScannableSafetyNumber(scannableSafetyNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,6 @@ package org.asamk.signal.manager.api;
|
||||
|
||||
public class InvalidNumberException extends Exception {
|
||||
|
||||
public InvalidNumberException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
InvalidNumberException(String message, Throwable e) {
|
||||
super(message, e);
|
||||
}
|
||||
|
||||
@ -6,30 +6,16 @@ import java.util.Optional;
|
||||
public record Message(
|
||||
String messageText,
|
||||
List<String> attachments,
|
||||
boolean viewOnce,
|
||||
boolean voiceNote,
|
||||
List<Mention> mentions,
|
||||
Optional<Quote> quote,
|
||||
Optional<Sticker> sticker,
|
||||
List<Preview> previews,
|
||||
Optional<StoryReply> storyReply,
|
||||
List<TextStyle> textStyles,
|
||||
boolean urgent
|
||||
Optional<StoryReply> storyReply
|
||||
) {
|
||||
|
||||
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
||||
|
||||
public record Quote(
|
||||
long timestamp,
|
||||
RecipientIdentifier.Single author,
|
||||
String message,
|
||||
List<Mention> mentions,
|
||||
List<TextStyle> textStyles,
|
||||
List<Attachment> attachments
|
||||
) {
|
||||
|
||||
public record Attachment(String contentType, String filename, String preview) {}
|
||||
}
|
||||
public record Quote(long timestamp, RecipientIdentifier.Single author, String message, List<Mention> mentions) {}
|
||||
|
||||
public record Sticker(byte[] packId, int stickerId) {}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.asamk.signal.manager.util.MimeUtils;
|
||||
import org.signal.libsignal.metadata.ProtocolException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
@ -35,13 +35,13 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRange;
|
||||
|
||||
public record MessageEnvelope(
|
||||
Optional<RecipientAddress> sourceAddress,
|
||||
int sourceDevice,
|
||||
@ -116,35 +116,17 @@ public record MessageEnvelope(
|
||||
Optional<Long> remoteDeleteId,
|
||||
Optional<Sticker> sticker,
|
||||
List<SharedContact> sharedContacts,
|
||||
Optional<PollCreate> pollCreate,
|
||||
Optional<PollVote> pollVote,
|
||||
Optional<PollTerminate> pollTerminate,
|
||||
List<Mention> mentions,
|
||||
List<Preview> previews,
|
||||
List<TextStyle> textStyles,
|
||||
Optional<PinMessage> pinMessage,
|
||||
Optional<UnpinMessage> unpinMessage,
|
||||
Optional<AdminDelete> adminDelete
|
||||
List<TextStyle> textStyles
|
||||
) {
|
||||
|
||||
static Data from(
|
||||
final SignalServiceDataMessage dataMessage,
|
||||
Map<String, String> longTexts,
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver,
|
||||
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(),
|
||||
dataMessage.getGroupContext().map(GroupContext::from),
|
||||
dataMessage.getStoryContext()
|
||||
@ -152,17 +134,15 @@ public record MessageEnvelope(
|
||||
recipientResolver,
|
||||
addressResolver)),
|
||||
dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
|
||||
body,
|
||||
dataMessage.getBody(),
|
||||
dataMessage.getExpiresInSeconds(),
|
||||
dataMessage.isExpirationUpdate(),
|
||||
dataMessage.isViewOnce(),
|
||||
false,
|
||||
dataMessage.isEndSession(),
|
||||
dataMessage.isProfileKeyUpdate(),
|
||||
dataMessage.getProfileKey().isPresent(),
|
||||
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
|
||||
dataMessage.getQuote()
|
||||
.filter(q -> q.getAuthor() != null && q.getAuthor().isValid())
|
||||
.map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
|
||||
dataMessage.getQuote().map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
|
||||
dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null),
|
||||
dataMessage.getAttachments()
|
||||
.map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
|
||||
@ -174,9 +154,6 @@ public record MessageEnvelope(
|
||||
.map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
|
||||
.toList())
|
||||
.orElse(List.of()),
|
||||
dataMessage.getPollCreate().map(PollCreate::from),
|
||||
dataMessage.getPollVote().map(p -> PollVote.from(p, recipientResolver, addressResolver)),
|
||||
dataMessage.getPollTerminate().map(PollTerminate::from),
|
||||
dataMessage.getMentions()
|
||||
.map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
|
||||
.orElse(List.of()),
|
||||
@ -184,11 +161,8 @@ public record MessageEnvelope(
|
||||
.map(a -> a.stream().map(preview -> Preview.from(preview, fileProvider)).toList())
|
||||
.orElse(List.of()),
|
||||
dataMessage.getBodyRanges()
|
||||
.map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList())
|
||||
.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)));
|
||||
.map(a -> a.stream().filter(BodyRange::hasStyle).map(TextStyle::from).toList())
|
||||
.orElse(List.of()));
|
||||
}
|
||||
|
||||
public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
|
||||
@ -263,23 +237,23 @@ public record MessageEnvelope(
|
||||
return new Quote(quote.getId(),
|
||||
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor()))
|
||||
.toApiRecipientAddress(),
|
||||
Optional.of(quote.getText()),
|
||||
Optional.ofNullable(quote.getText()),
|
||||
quote.getMentions() == null
|
||||
? List.of()
|
||||
: quote.getMentions()
|
||||
.stream()
|
||||
.map(m -> Mention.from(m, recipientResolver, addressResolver))
|
||||
.toList(),
|
||||
.stream()
|
||||
.map(m -> Mention.from(m, recipientResolver, addressResolver))
|
||||
.toList(),
|
||||
quote.getAttachments() == null
|
||||
? List.of()
|
||||
: quote.getAttachments().stream().map(a -> Attachment.from(a, fileProvider)).toList(),
|
||||
quote.getBodyRanges() == null
|
||||
? List.of()
|
||||
: quote.getBodyRanges()
|
||||
.stream()
|
||||
.filter(r -> r.style != null)
|
||||
.map(TextStyle::from)
|
||||
.toList());
|
||||
.stream()
|
||||
.filter(BodyRange::hasStyle)
|
||||
.map(TextStyle::from)
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,9 +294,9 @@ public record MessageEnvelope(
|
||||
boolean isBorderless
|
||||
) {
|
||||
|
||||
static Attachment from(SignalServiceAttachment signalAttachment, AttachmentFileProvider fileProvider) {
|
||||
if (signalAttachment.isPointer()) {
|
||||
final var a = signalAttachment.asPointer();
|
||||
static Attachment from(SignalServiceAttachment attachment, AttachmentFileProvider fileProvider) {
|
||||
if (attachment.isPointer()) {
|
||||
final var a = attachment.asPointer();
|
||||
final var attachmentFile = fileProvider.getFile(a);
|
||||
return new Attachment(Optional.of(attachmentFile.getName()),
|
||||
Optional.of(attachmentFile),
|
||||
@ -339,32 +313,26 @@ public record MessageEnvelope(
|
||||
a.isGif(),
|
||||
a.isBorderless());
|
||||
} else {
|
||||
Attachment attachment = null;
|
||||
try (final var a = signalAttachment.asStream()) {
|
||||
attachment = new Attachment(Optional.empty(),
|
||||
Optional.empty(),
|
||||
a.getFileName(),
|
||||
a.getContentType(),
|
||||
a.getUploadTimestamp() == 0 ? Optional.empty() : Optional.of(a.getUploadTimestamp()),
|
||||
Optional.of(a.getLength()),
|
||||
a.getPreview(),
|
||||
Optional.empty(),
|
||||
a.getCaption(),
|
||||
a.getWidth() == 0 ? Optional.empty() : Optional.of(a.getWidth()),
|
||||
a.getHeight() == 0 ? Optional.empty() : Optional.of(a.getHeight()),
|
||||
a.getVoiceNote(),
|
||||
a.isGif(),
|
||||
a.isBorderless());
|
||||
return attachment;
|
||||
} catch (IOException e) {
|
||||
return attachment;
|
||||
}
|
||||
final var a = attachment.asStream();
|
||||
return new Attachment(Optional.empty(),
|
||||
Optional.empty(),
|
||||
a.getFileName(),
|
||||
a.getContentType(),
|
||||
a.getUploadTimestamp() == 0 ? Optional.empty() : Optional.of(a.getUploadTimestamp()),
|
||||
Optional.of(a.getLength()),
|
||||
a.getPreview(),
|
||||
Optional.empty(),
|
||||
a.getCaption(),
|
||||
a.getWidth() == 0 ? Optional.empty() : Optional.of(a.getWidth()),
|
||||
a.getHeight() == 0 ? Optional.empty() : Optional.of(a.getHeight()),
|
||||
a.getVoiceNote(),
|
||||
a.isGif(),
|
||||
a.isBorderless());
|
||||
}
|
||||
}
|
||||
|
||||
static Attachment from(
|
||||
SignalServiceDataMessage.Quote.QuotedAttachment a,
|
||||
final AttachmentFileProvider fileProvider
|
||||
SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
return new Attachment(Optional.empty(),
|
||||
Optional.empty(),
|
||||
@ -416,7 +384,7 @@ public record MessageEnvelope(
|
||||
}
|
||||
|
||||
public record Name(
|
||||
Optional<String> nickname,
|
||||
Optional<String> display,
|
||||
Optional<String> given,
|
||||
Optional<String> family,
|
||||
Optional<String> prefix,
|
||||
@ -425,7 +393,7 @@ public record MessageEnvelope(
|
||||
) {
|
||||
|
||||
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.getFamily(),
|
||||
name.getPrefix(),
|
||||
@ -507,15 +475,15 @@ public record MessageEnvelope(
|
||||
) {
|
||||
|
||||
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.getStreet(),
|
||||
address.getPobox(),
|
||||
address.getNeighborhood(),
|
||||
address.getCity(),
|
||||
address.getRegion(),
|
||||
address.getPostcode(),
|
||||
address.getCountry());
|
||||
address.getLabel(),
|
||||
address.getLabel(),
|
||||
address.getLabel(),
|
||||
address.getLabel(),
|
||||
address.getLabel(),
|
||||
address.getLabel(),
|
||||
address.getLabel());
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
@ -534,47 +502,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) {
|
||||
|
||||
static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) {
|
||||
static Preview from(
|
||||
SignalServicePreview preview, final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
return new Preview(preview.getTitle(),
|
||||
preview.getDescription(),
|
||||
preview.getDate(),
|
||||
@ -583,68 +515,44 @@ public record MessageEnvelope(
|
||||
}
|
||||
}
|
||||
|
||||
public record PinMessage(
|
||||
RecipientAddress targetAuthor, long targetSentTimestamp, long pinDurationSeconds
|
||||
) {
|
||||
public record TextStyle(Style style, int start, int length) {
|
||||
|
||||
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 enum Style {
|
||||
NONE,
|
||||
BOLD,
|
||||
ITALIC,
|
||||
SPOILER,
|
||||
STRIKETHROUGH,
|
||||
MONOSPACE;
|
||||
|
||||
static Style from(BodyRange.Style style) {
|
||||
return switch (style) {
|
||||
case NONE -> NONE;
|
||||
case BOLD -> BOLD;
|
||||
case ITALIC -> ITALIC;
|
||||
case SPOILER -> SPOILER;
|
||||
case STRIKETHROUGH -> STRIKETHROUGH;
|
||||
case MONOSPACE -> MONOSPACE;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static TextStyle from(BodyRange bodyRange) {
|
||||
return new TextStyle(Style.from(bodyRange.getStyle()), bodyRange.getStart(), bodyRange.getLength());
|
||||
}
|
||||
}
|
||||
|
||||
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 static Edit from(
|
||||
final SignalServiceEditMessage editMessage,
|
||||
Map<String, String> longTexts,
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver,
|
||||
final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
return new Edit(editMessage.getTargetSentTimestamp(),
|
||||
Data.from(editMessage.getDataMessage(),
|
||||
longTexts,
|
||||
recipientResolver,
|
||||
addressResolver,
|
||||
fileProvider));
|
||||
Data.from(editMessage.getDataMessage(), recipientResolver, addressResolver, fileProvider));
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,13 +569,12 @@ public record MessageEnvelope(
|
||||
|
||||
public static Sync from(
|
||||
final SignalServiceSyncMessage syncMessage,
|
||||
Map<String, String> longTexts,
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver,
|
||||
final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
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.getRead()
|
||||
.map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
|
||||
@ -696,7 +603,6 @@ public record MessageEnvelope(
|
||||
|
||||
static Sent from(
|
||||
SentTranscriptMessage sentMessage,
|
||||
Map<String, String> longTexts,
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver,
|
||||
final AttachmentFileProvider fileProvider
|
||||
@ -712,17 +618,9 @@ public record MessageEnvelope(
|
||||
.toApiRecipientAddress())
|
||||
.collect(Collectors.toSet()),
|
||||
sentMessage.getDataMessage()
|
||||
.map(message -> Data.from(message,
|
||||
longTexts,
|
||||
recipientResolver,
|
||||
addressResolver,
|
||||
fileProvider)),
|
||||
.map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)),
|
||||
sentMessage.getEditMessage()
|
||||
.map(message -> Edit.from(message,
|
||||
longTexts,
|
||||
recipientResolver,
|
||||
addressResolver,
|
||||
fileProvider)),
|
||||
.map(message -> Edit.from(message, recipientResolver, addressResolver, fileProvider)),
|
||||
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
|
||||
}
|
||||
}
|
||||
@ -734,15 +632,11 @@ public record MessageEnvelope(
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver
|
||||
) {
|
||||
return new Blocked(blockedListMessage.individuals.stream()
|
||||
.map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(),
|
||||
null,
|
||||
d.getE164(),
|
||||
null))
|
||||
.toList(),
|
||||
blockedListMessage.groups.stream()
|
||||
.map(group -> GroupId.unknownVersion(group.getGroupId()))
|
||||
.toList());
|
||||
return new Blocked(blockedListMessage.getAddresses()
|
||||
.stream()
|
||||
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
|
||||
.toApiRecipientAddress())
|
||||
.toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
|
||||
}
|
||||
}
|
||||
|
||||
@ -753,7 +647,7 @@ public record MessageEnvelope(
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver
|
||||
) {
|
||||
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSenderAci()))
|
||||
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
|
||||
.toApiRecipientAddress(), readMessage.getTimestamp());
|
||||
}
|
||||
}
|
||||
@ -816,9 +710,7 @@ public record MessageEnvelope(
|
||||
DELETE,
|
||||
BLOCK,
|
||||
BLOCK_AND_DELETE,
|
||||
UNBLOCK_AND_ACCEPT,
|
||||
SPAM,
|
||||
BLOCK_AND_SPAM;
|
||||
UNBLOCK_AND_ACCEPT;
|
||||
|
||||
static Type from(MessageRequestResponseMessage.Type type) {
|
||||
return switch (type) {
|
||||
@ -828,8 +720,6 @@ public record MessageEnvelope(
|
||||
case BLOCK -> BLOCK;
|
||||
case BLOCK_AND_DELETE -> BLOCK_AND_DELETE;
|
||||
case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT;
|
||||
case SPAM -> SPAM;
|
||||
case BLOCK_AND_SPAM -> BLOCK_AND_SPAM;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -845,8 +735,7 @@ public record MessageEnvelope(
|
||||
Optional<Hangup> hangup,
|
||||
Optional<Busy> busy,
|
||||
List<IceUpdate> iceUpdate,
|
||||
Optional<Opaque> opaque,
|
||||
boolean isUrgent
|
||||
Optional<Opaque> opaque
|
||||
) {
|
||||
|
||||
public static Call from(final SignalServiceCallMessage callMessage) {
|
||||
@ -860,14 +749,16 @@ public record MessageEnvelope(
|
||||
callMessage.getIceUpdateMessages()
|
||||
.map(m -> m.stream().map(IceUpdate::from).toList())
|
||||
.orElse(List.of()),
|
||||
callMessage.getOpaqueMessage().map(Opaque::from),
|
||||
callMessage.isUrgent());
|
||||
callMessage.getOpaqueMessage().map(Opaque::from));
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
@ -883,10 +774,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) {
|
||||
return new Answer(answerMessage.getId(), answerMessage.getOpaque());
|
||||
return new Answer(answerMessage.getId(), answerMessage.getSdp(), answerMessage.getOpaque());
|
||||
}
|
||||
}
|
||||
|
||||
@ -897,12 +788,13 @@ public record MessageEnvelope(
|
||||
}
|
||||
}
|
||||
|
||||
public record Hangup(long id, Type type, int deviceId) {
|
||||
public record Hangup(long id, Type type, int deviceId, boolean isLegacy) {
|
||||
|
||||
static Hangup from(HangupMessage hangupMessage) {
|
||||
return new Hangup(hangupMessage.getId(),
|
||||
Type.from(hangupMessage.getType()),
|
||||
hangupMessage.getDeviceId());
|
||||
hangupMessage.getDeviceId(),
|
||||
hangupMessage.isLegacy());
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
@ -924,10 +816,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) {
|
||||
return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getOpaque());
|
||||
return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getSdp(), iceUpdateMessage.getOpaque());
|
||||
}
|
||||
}
|
||||
|
||||
@ -958,7 +850,9 @@ public record MessageEnvelope(
|
||||
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),
|
||||
storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
|
||||
storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
|
||||
@ -976,8 +870,7 @@ public record MessageEnvelope(
|
||||
) {
|
||||
|
||||
static TextAttachment from(
|
||||
SignalServiceTextAttachment textAttachment,
|
||||
final AttachmentFileProvider fileProvider
|
||||
SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
return new TextAttachment(textAttachment.getText(),
|
||||
textAttachment.getStyle().map(Style::from),
|
||||
@ -1024,24 +917,22 @@ public record MessageEnvelope(
|
||||
public static MessageEnvelope from(
|
||||
SignalServiceEnvelope envelope,
|
||||
SignalServiceContent content,
|
||||
Map<String, String> longTexts,
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver,
|
||||
final AttachmentFileProvider fileProvider,
|
||||
Exception exception
|
||||
) {
|
||||
final var serviceId = envelope.getSourceServiceId();
|
||||
final var source = !envelope.isUnidentifiedSender() && serviceId != null
|
||||
? recipientResolver.resolveRecipient(serviceId)
|
||||
final var source = !envelope.isUnidentifiedSender() && envelope.hasSourceUuid()
|
||||
? recipientResolver.resolveRecipient(envelope.getSourceAddress())
|
||||
: envelope.isUnidentifiedSender() && content != null
|
||||
? recipientResolver.resolveRecipient(content.getSender())
|
||||
? recipientResolver.resolveRecipient(content.getSender())
|
||||
: exception instanceof ProtocolException e
|
||||
? recipientResolver.resolveRecipient(e.getSender())
|
||||
? recipientResolver.resolveRecipient(e.getSender())
|
||||
: null;
|
||||
final var sourceDevice = envelope.hasSourceDevice()
|
||||
? envelope.getSourceDevice()
|
||||
: content != null
|
||||
? content.getSenderDevice()
|
||||
? content.getSenderDevice()
|
||||
: exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
|
||||
|
||||
Optional<Receipt> receipt;
|
||||
@ -1055,15 +946,9 @@ public record MessageEnvelope(
|
||||
receipt = content.getReceiptMessage().map(Receipt::from);
|
||||
typing = content.getTypingMessage().map(Typing::from);
|
||||
data = content.getDataMessage()
|
||||
.map(dataMessage -> Data.from(dataMessage,
|
||||
longTexts,
|
||||
recipientResolver,
|
||||
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));
|
||||
.map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider));
|
||||
edit = content.getEditMessage().map(s -> Edit.from(s, recipientResolver, addressResolver, fileProvider));
|
||||
sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider));
|
||||
call = content.getCallMessage().map(Call::from);
|
||||
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
|
||||
} else {
|
||||
|
||||
@ -3,16 +3,5 @@ package org.asamk.signal.manager.api;
|
||||
public enum PhoneNumberSharingMode {
|
||||
EVERYBODY,
|
||||
CONTACTS,
|
||||
NOBODY;
|
||||
|
||||
public static PhoneNumberSharingMode valueOfOrNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return valueOf(value);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
NOBODY,
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
public class PinLockMissingException extends Exception {}
|
||||
@ -10,19 +10,12 @@ public class ProofRequiredException extends Exception {
|
||||
|
||||
private final String token;
|
||||
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) {
|
||||
super("Rate limit");
|
||||
this.token = token;
|
||||
this.options = options;
|
||||
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 ProofRequiredException(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
|
||||
this.token = e.getToken();
|
||||
this.options = e.getOptions().stream().map(Option::from).collect(Collectors.toSet());
|
||||
this.retryAfterSeconds = e.getRetryAfterSeconds();
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
@ -33,17 +26,17 @@ public class ProofRequiredException extends Exception {
|
||||
return options;
|
||||
}
|
||||
|
||||
public long getRetryAfterMilliseconds() {
|
||||
return retryAfterMilliseconds;
|
||||
public long getRetryAfterSeconds() {
|
||||
return retryAfterSeconds;
|
||||
}
|
||||
|
||||
public enum Option {
|
||||
CAPTCHA,
|
||||
RECAPTCHA,
|
||||
PUSH_CHALLENGE;
|
||||
|
||||
static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) {
|
||||
return switch (option) {
|
||||
case CAPTCHA -> CAPTCHA;
|
||||
case RECAPTCHA -> RECAPTCHA;
|
||||
case PUSH_CHALLENGE -> PUSH_CHALLENGE;
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,18 +2,14 @@ package org.asamk.signal.manager.api;
|
||||
|
||||
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");
|
||||
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
||||
}
|
||||
|
||||
public static RateLimitException from(org.whispersystems.signalservice.api.push.exceptions.RateLimitException e) {
|
||||
return new RateLimitException(e.getRetryAfterMilliseconds().orElse(null));
|
||||
}
|
||||
|
||||
public Long getRetryAfterMilliseconds() {
|
||||
return retryAfterMilliseconds;
|
||||
public long getNextAttemptTimestamp() {
|
||||
return nextAttemptTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
public record ReceiveConfig(
|
||||
boolean ignoreAttachments,
|
||||
boolean ignoreStories,
|
||||
boolean ignoreAvatars,
|
||||
boolean ignoreStickers,
|
||||
boolean sendReadReceipts
|
||||
) {}
|
||||
public record ReceiveConfig(boolean ignoreAttachments, boolean ignoreStories, boolean sendReadReceipts) {}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.asamk.signal.manager.storage.recipients.Contact;
|
||||
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
@ -20,16 +22,13 @@ public class Recipient {
|
||||
|
||||
private final Profile profile;
|
||||
|
||||
private final Boolean discoverable;
|
||||
|
||||
public Recipient(
|
||||
final RecipientId recipientId,
|
||||
final RecipientAddress address,
|
||||
final Contact contact,
|
||||
final ProfileKey profileKey,
|
||||
final ExpiringProfileKeyCredential expiringProfileKeyCredential,
|
||||
final Profile profile,
|
||||
final Boolean discoverable
|
||||
final Profile profile
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.address = address;
|
||||
@ -37,7 +36,6 @@ public class Recipient {
|
||||
this.profileKey = profileKey;
|
||||
this.expiringProfileKeyCredential = expiringProfileKeyCredential;
|
||||
this.profile = profile;
|
||||
this.discoverable = discoverable;
|
||||
}
|
||||
|
||||
private Recipient(final Builder builder) {
|
||||
@ -45,9 +43,8 @@ public class Recipient {
|
||||
address = builder.address;
|
||||
contact = builder.contact;
|
||||
profileKey = builder.profileKey;
|
||||
expiringProfileKeyCredential = builder.expiringProfileKeyCredential;
|
||||
expiringProfileKeyCredential = builder.expiringProfileKeyCredential1;
|
||||
profile = builder.profile;
|
||||
discoverable = builder.discoverable;
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
@ -60,7 +57,7 @@ public class Recipient {
|
||||
builder.address = copy.getAddress();
|
||||
builder.contact = copy.getContact();
|
||||
builder.profileKey = copy.getProfileKey();
|
||||
builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
|
||||
builder.expiringProfileKeyCredential1 = copy.getExpiringProfileKeyCredential();
|
||||
builder.profile = copy.getProfile();
|
||||
return builder;
|
||||
}
|
||||
@ -89,10 +86,6 @@ public class Recipient {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public Boolean getDiscoverable() {
|
||||
return discoverable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
@ -117,9 +110,8 @@ public class Recipient {
|
||||
private RecipientAddress address;
|
||||
private Contact contact;
|
||||
private ProfileKey profileKey;
|
||||
private ExpiringProfileKeyCredential expiringProfileKeyCredential;
|
||||
private ExpiringProfileKeyCredential expiringProfileKeyCredential1;
|
||||
private Profile profile;
|
||||
private Boolean discoverable;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
@ -145,7 +137,7 @@ public class Recipient {
|
||||
}
|
||||
|
||||
public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) {
|
||||
expiringProfileKeyCredential = val;
|
||||
expiringProfileKeyCredential1 = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -154,11 +146,6 @@ public class Recipient {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withDiscoverable(final Boolean val) {
|
||||
discoverable = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Recipient build() {
|
||||
return new Recipient(this);
|
||||
}
|
||||
|
||||
@ -1,55 +1,49 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.signal.core.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public record RecipientAddress(
|
||||
Optional<String> aci, Optional<String> pni, Optional<String> number, Optional<String> username
|
||||
) {
|
||||
public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Optional<String> username) {
|
||||
|
||||
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
|
||||
public static final UUID UNKNOWN_UUID = ServiceId.UNKNOWN.uuid();
|
||||
|
||||
/**
|
||||
* Construct a RecipientAddress.
|
||||
*
|
||||
* @param aci The ACI of the user, if available.
|
||||
* @param pni The PNI of the user, if available.
|
||||
* @param uuid The UUID of the user, if available.
|
||||
* @param number The phone number of the user, if available.
|
||||
*/
|
||||
public RecipientAddress {
|
||||
if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) {
|
||||
throw new AssertionError("Must have either a ACI, PNI, username or E164 number!");
|
||||
uuid = uuid.isPresent() && uuid.get().equals(UNKNOWN_UUID) ? Optional.empty() : uuid;
|
||||
if (uuid.isEmpty() && number.isEmpty() && username.isEmpty()) {
|
||||
throw new AssertionError("Must have either a UUID or E164 number!");
|
||||
}
|
||||
}
|
||||
|
||||
public RecipientAddress(String e164) {
|
||||
this(null, null, e164, null);
|
||||
public RecipientAddress(UUID uuid, String e164) {
|
||||
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().uuid()), address.getNumber(), Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress(UUID uuid) {
|
||||
this(uuid.toString(), null, null, null);
|
||||
}
|
||||
|
||||
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);
|
||||
this(Optional.of(uuid), Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
if (aci.isPresent()) {
|
||||
return aci.get();
|
||||
if (uuid.isPresent()) {
|
||||
return uuid.get().toString();
|
||||
} else if (number.isPresent()) {
|
||||
return number.get();
|
||||
} else if (pni.isPresent()) {
|
||||
return pni.get();
|
||||
} else if (username.isPresent()) {
|
||||
return username.get();
|
||||
} else {
|
||||
@ -60,16 +54,17 @@ public record RecipientAddress(
|
||||
public String getLegacyIdentifier() {
|
||||
if (number.isPresent()) {
|
||||
return number.get();
|
||||
} else if (uuid.isPresent()) {
|
||||
return uuid.get().toString();
|
||||
} else if (username.isPresent()) {
|
||||
return username.get();
|
||||
} else {
|
||||
return getIdentifier();
|
||||
throw new AssertionError("Given the checks in the constructor, this should not be possible.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean matches(RecipientAddress other) {
|
||||
return (aci.isPresent() && other.aci.isPresent() && aci.get().equals(other.aci.get()))
|
||||
|| (
|
||||
pni.isPresent() && other.pni.isPresent() && pni.get().equals(other.pni.get())
|
||||
)
|
||||
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get()))
|
||||
|| (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get()))
|
||||
|| (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get()));
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
||||
import org.signal.core.util.UuidUtil;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@ -24,37 +25,31 @@ public sealed interface RecipientIdentifier {
|
||||
sealed interface Single extends RecipientIdentifier {
|
||||
|
||||
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
|
||||
if (UuidUtil.INSTANCE.isUuid(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");
|
||||
try {
|
||||
if (UuidUtil.isUuid(identifier)) {
|
||||
return new Uuid(UUID.fromString(identifier));
|
||||
}
|
||||
return new Pni(UUID.fromString(pni));
|
||||
}
|
||||
|
||||
if (identifier.startsWith("u:")) {
|
||||
return new Username(identifier.substring(2));
|
||||
}
|
||||
if (identifier.startsWith("u:")) {
|
||||
return new Username(identifier.substring(2));
|
||||
}
|
||||
|
||||
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
||||
if (!normalizedNumber.equals(identifier)) {
|
||||
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
||||
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
|
||||
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
||||
if (!normalizedNumber.equals(identifier)) {
|
||||
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
||||
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
|
||||
}
|
||||
return new Number(normalizedNumber);
|
||||
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
|
||||
throw new InvalidNumberException(e.getMessage(), e);
|
||||
}
|
||||
return new Number(normalizedNumber);
|
||||
}
|
||||
|
||||
static Single fromAddress(RecipientAddress address) {
|
||||
if (address.number().isPresent()) {
|
||||
return new Number(address.number().get());
|
||||
} else if (address.aci().isPresent()) {
|
||||
return new Uuid(UUID.fromString(address.aci().get()));
|
||||
} else if (address.pni().isPresent()) {
|
||||
return new Pni(UUID.fromString(address.pni().get().substring(4)));
|
||||
} else if (address.uuid().isPresent()) {
|
||||
return new Uuid(address.uuid().get());
|
||||
} else if (address.username().isPresent()) {
|
||||
return new Username(address.username().get());
|
||||
}
|
||||
@ -77,19 +72,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 {
|
||||
|
||||
@Override
|
||||
@ -99,7 +81,7 @@ public sealed interface RecipientIdentifier {
|
||||
|
||||
@Override
|
||||
public RecipientAddress toPartialRecipientAddress() {
|
||||
return new RecipientAddress(number);
|
||||
return new RecipientAddress(null, number);
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +94,7 @@ public sealed interface RecipientIdentifier {
|
||||
|
||||
@Override
|
||||
public RecipientAddress toPartialRecipientAddress() {
|
||||
return new RecipientAddress(null, null, null, username);
|
||||
return new RecipientAddress(null, null, username);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
|
||||
|
||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
|
||||
public record SendMessageResult(
|
||||
RecipientAddress address,
|
||||
@ -9,13 +10,30 @@ public record SendMessageResult(
|
||||
boolean isNetworkFailure,
|
||||
boolean isUnregisteredFailure,
|
||||
boolean isIdentityFailure,
|
||||
RateLimitException rateLimitException,
|
||||
ProofRequiredException proofRequiredFailure,
|
||||
boolean isInvalidPreKeyFailure
|
||||
boolean isRateLimitFailure,
|
||||
ProofRequiredException proofRequiredFailure
|
||||
) {
|
||||
|
||||
public static SendMessageResult success(RecipientAddress address) {
|
||||
return new SendMessageResult(address, true, false, false, false, false, null);
|
||||
}
|
||||
|
||||
public static SendMessageResult networkFailure(RecipientAddress address) {
|
||||
return new SendMessageResult(address, false, true, false, false, false, null);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static SendMessageResult identityFailure(RecipientAddress address, IdentityKey identityKey) {
|
||||
return new SendMessageResult(address, false, false, false, true, false, null);
|
||||
}
|
||||
|
||||
public static SendMessageResult proofRequiredFailure(
|
||||
RecipientAddress address, ProofRequiredException proofRequiredException
|
||||
) {
|
||||
return new SendMessageResult(address, false, true, false, false, false, proofRequiredException);
|
||||
}
|
||||
|
||||
public static SendMessageResult from(
|
||||
@ -23,30 +41,15 @@ public record SendMessageResult(
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver
|
||||
) {
|
||||
final var rateLimitFailure = sendMessageResult.getRateLimitFailure();
|
||||
final var proofRequiredFailure = sendMessageResult.getProofRequiredFailure();
|
||||
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||
sendMessageResult.getAddress())).toApiRecipientAddress(),
|
||||
sendMessageResult.isSuccess(),
|
||||
sendMessageResult.isNetworkFailure(),
|
||||
sendMessageResult.isUnregisteredFailure(),
|
||||
sendMessageResult.getIdentityFailure() != null,
|
||||
rateLimitFailure == null ? null : RateLimitException.from(rateLimitFailure),
|
||||
proofRequiredFailure == null ? null : ProofRequiredException.from(proofRequiredFailure),
|
||||
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;
|
||||
}
|
||||
sendMessageResult.getRateLimitFailure() != null || sendMessageResult.getProofRequiredFailure() != null,
|
||||
sendMessageResult.getProofRequiredFailure() == null
|
||||
? null
|
||||
: new ProofRequiredException(sendMessageResult.getProofRequiredFailure()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
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)
|
||||
&& 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.signal.core.util.Hex;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class StickerPackId {
|
||||
@ -34,9 +32,4 @@ public class StickerPackId {
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StickerPackId{" + Hex.toStringCondensed(id) + '}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
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.net.URI;
|
||||
@ -11,7 +11,10 @@ import java.nio.charset.StandardCharsets;
|
||||
|
||||
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.
|
||||
@ -19,7 +22,7 @@ public record StickerPackUrl(StickerPackId packId, byte[] packKey) {
|
||||
public static StickerPackUrl fromUri(URI uri) throws InvalidStickerPackLinkException {
|
||||
final var rawQuery = uri.getRawFragment();
|
||||
if (isEmpty(rawQuery)) {
|
||||
throw new InvalidStickerPackLinkException("Invalid sticker pack uri");
|
||||
throw new RuntimeException("Invalid sticker pack uri");
|
||||
}
|
||||
|
||||
var query = Utils.getQueryMap(rawQuery);
|
||||
@ -45,6 +48,11 @@ public record StickerPackUrl(StickerPackId packId, byte[] packKey) {
|
||||
return new StickerPackUrl(packId, packKey);
|
||||
}
|
||||
|
||||
public StickerPackUrl(final StickerPackId packId, final byte[] packKey) {
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
}
|
||||
|
||||
public URI getUrl() {
|
||||
try {
|
||||
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) {
|
||||
super(message);
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange;
|
||||
|
||||
public record TextStyle(Style style, Integer start, Integer length) {
|
||||
|
||||
public enum Style {
|
||||
NONE,
|
||||
BOLD,
|
||||
ITALIC,
|
||||
SPOILER,
|
||||
STRIKETHROUGH,
|
||||
MONOSPACE;
|
||||
|
||||
static Style fromInternal(BodyRange.Style style) {
|
||||
if (style == null) {
|
||||
return NONE;
|
||||
}
|
||||
return switch (style) {
|
||||
case NONE -> NONE;
|
||||
case BOLD -> BOLD;
|
||||
case ITALIC -> ITALIC;
|
||||
case SPOILER -> SPOILER;
|
||||
case STRIKETHROUGH -> STRIKETHROUGH;
|
||||
case MONOSPACE -> MONOSPACE;
|
||||
};
|
||||
}
|
||||
|
||||
public static Style from(String style) {
|
||||
return switch (style) {
|
||||
case "NONE" -> NONE;
|
||||
case "BOLD" -> BOLD;
|
||||
case "ITALIC" -> ITALIC;
|
||||
case "SPOILER" -> SPOILER;
|
||||
case "STRIKETHROUGH" -> STRIKETHROUGH;
|
||||
case "MONOSPACE" -> MONOSPACE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
BodyRange.Style toBodyRangeStyle() {
|
||||
return switch (this) {
|
||||
case NONE -> BodyRange.Style.NONE;
|
||||
case BOLD -> BodyRange.Style.BOLD;
|
||||
case ITALIC -> BodyRange.Style.ITALIC;
|
||||
case SPOILER -> BodyRange.Style.SPOILER;
|
||||
case STRIKETHROUGH -> BodyRange.Style.STRIKETHROUGH;
|
||||
case MONOSPACE -> BodyRange.Style.MONOSPACE;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static TextStyle from(BodyRange bodyRange) {
|
||||
return new TextStyle(Style.fromInternal(bodyRange.style), bodyRange.start, bodyRange.length);
|
||||
}
|
||||
|
||||
public BodyRange toBodyRange() {
|
||||
return new BodyRange.Builder().start(this.start())
|
||||
.length(this.length())
|
||||
.style(this.style().toBodyRangeStyle())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
|
||||
public enum TrustLevel {
|
||||
UNTRUSTED,
|
||||
@ -16,6 +17,15 @@ public enum TrustLevel {
|
||||
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;
|
||||
case UNRECOGNIZED -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) {
|
||||
return switch (verifiedState) {
|
||||
case DEFAULT -> TRUSTED_UNVERIFIED;
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TurnServer(
|
||||
String username, String password, List<String> urls
|
||||
) {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user