mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-08-30 06:16:02 +00:00
Compare commits
256 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c17e01c37 | ||
|
|
9605c94f09 | ||
|
|
d2f5696df1 | ||
|
|
81e513d6b7 | ||
|
|
dbf11fd006 | ||
|
|
7e802166a3 | ||
|
|
0f88410465 | ||
|
|
13819c8f09 | ||
|
|
85580d1ff6 | ||
|
|
f8ea8ed314 | ||
|
|
472784e614 | ||
|
|
db97731f63 | ||
|
|
ff9f2028ae | ||
|
|
9ee5262876 | ||
|
|
b01b6b370d | ||
|
|
890f798a03 | ||
|
|
3c319f8b4a | ||
|
|
fd3f572499 | ||
|
|
854cb35d15 | ||
|
|
e78521bb5c | ||
|
|
86ad7bf53f | ||
|
|
de5daf0ad4 | ||
|
|
ecc76ec52b | ||
|
|
6fa1d1bf90 | ||
|
|
d29f6c1641 | ||
|
|
b3e3eda4f7 | ||
|
|
1e59c6f6cc | ||
|
|
457658aca0 | ||
|
|
2cf5e25e9e | ||
|
|
151b42c6e9 | ||
|
|
91138452bb | ||
|
|
304ffb0e33 | ||
|
|
987832099d | ||
|
|
64a629002d | ||
|
|
c26e0632c5 | ||
|
|
21dd00d96a | ||
|
|
95c6dae927 | ||
|
|
bb2409a7f2 | ||
|
|
89756bb7a7 | ||
|
|
6808b66897 | ||
|
|
14f98602d2 | ||
|
|
fd98cce8b3 | ||
|
|
4f4a4b9fb4 | ||
|
|
e15c29e4e8 | ||
|
|
2e367644d9 | ||
|
|
78dba20a59 | ||
|
|
b48ccf2605 | ||
|
|
54d713ea7d | ||
|
|
4dda7582a3 | ||
|
|
248c0c0dab | ||
|
|
e70bddd790 | ||
|
|
ac5ed431d3 | ||
|
|
fb10e1a501 | ||
|
|
2a744edd43 | ||
|
|
c95a67862e | ||
|
|
5a525f51f6 | ||
|
|
eab6ffbd8c | ||
|
|
8809b90b72 | ||
|
|
98d9960e66 | ||
|
|
254efba643 | ||
|
|
bb433ebe59 | ||
|
|
5ac06a5ccd | ||
|
|
debb8a20e6 | ||
|
|
6bef205b3f | ||
|
|
443b4da8f8 | ||
|
|
afe8c24665 | ||
|
|
9f0676d563 | ||
|
|
b3c1b6a4f6 | ||
|
|
bf1376d74d | ||
|
|
de678596c6 | ||
|
|
29e65a5c8e | ||
|
|
60779c91c6 | ||
|
|
bda4e7fc0f | ||
|
|
12ffc34967 | ||
|
|
9e3585dcce | ||
|
|
46c61c5aac | ||
|
|
8d6264e02e | ||
|
|
f057c5031c | ||
|
|
40b1928844 | ||
|
|
2a827f1285 | ||
|
|
ced9560040 | ||
|
|
44d54b3215 | ||
|
|
393e1efcd1 | ||
|
|
f34b552054 | ||
|
|
46ce552589 | ||
|
|
6da5c37504 | ||
|
|
4601e60118 | ||
|
|
fcf82b9318 | ||
|
|
9c8137fafa | ||
|
|
0a1531dcce | ||
|
|
c10f618a3e | ||
|
|
4a3d9d90a6 | ||
|
|
b4275414e1 | ||
|
|
5f94b7b6d1 | ||
|
|
dc43e44020 | ||
|
|
251bd2d87a | ||
|
|
a3fcda7598 | ||
|
|
c9e2504349 | ||
|
|
9b09df5f17 | ||
|
|
5fe94ff44a | ||
|
|
6286a054eb | ||
|
|
e6635d1bb0 | ||
|
|
056878fad7 | ||
|
|
da214817be | ||
|
|
d6edaf3be2 | ||
|
|
47e50988b5 | ||
|
|
aa446619f2 | ||
|
|
6405655127 | ||
|
|
b8d990b0f9 | ||
|
|
417d2ce971 | ||
|
|
33b2b563b3 | ||
|
|
740cd6f89b | ||
|
|
7887ed408d | ||
|
|
ddfad2c4ce | ||
|
|
7e95ea7403 | ||
|
|
2991cdafe7 | ||
|
|
561dfc373f | ||
|
|
5bfb044245 | ||
|
|
e1b17bf863 | ||
|
|
aafb40fd94 | ||
|
|
7dc55eba81 | ||
|
|
a03d17a9e4 | ||
|
|
364f89f1d0 | ||
|
|
d0ee90dbbc | ||
|
|
398faa50b0 | ||
|
|
e9eabbeeb5 | ||
|
|
132dfb95dc | ||
|
|
2651823d4d | ||
|
|
4709cfacc7 | ||
|
|
9bc4c0ecd8 | ||
|
|
763ddf85e6 | ||
|
|
b2bab0d0dc | ||
|
|
62fc96c4c9 | ||
|
|
2667688139 | ||
|
|
990d1eab58 | ||
|
|
e6b33b8da7 | ||
|
|
d40f62ec21 | ||
|
|
265369e353 | ||
|
|
d1106299fe | ||
|
|
7919a0f4aa | ||
|
|
7a8a34f45e | ||
|
|
0a777ea7df | ||
|
|
103a0807ca | ||
|
|
135d3a1677 | ||
|
|
59a0bd87cd | ||
|
|
c94da00212 | ||
|
|
36649a8526 | ||
|
|
3297acd3f4 | ||
|
|
3e5caf9284 | ||
|
|
53ae9e4266 | ||
|
|
313f5392ef | ||
|
|
7d89375d3a | ||
|
|
db0f660d08 | ||
|
|
b498d2050a | ||
|
|
27a722dc75 | ||
|
|
7014f629fe | ||
|
|
30b57bdb3d | ||
|
|
b94162afbc | ||
|
|
2885ffeee8 | ||
|
|
6071291f16 | ||
|
|
37b8a4a996 | ||
|
|
af56a28b94 | ||
|
|
dfc7e3b495 | ||
|
|
e9114ae8fc | ||
|
|
f77a74d93f | ||
|
|
5cda87ee0e | ||
|
|
7384407823 | ||
|
|
7fa56a37fd | ||
|
|
8fcd953ece | ||
|
|
775236efc3 | ||
|
|
4b8dec26a9 | ||
|
|
d94e05c38c | ||
|
|
1bbf98fac0 | ||
|
|
c70515035f | ||
|
|
a9d235b7f1 | ||
|
|
92ded3fdf2 | ||
|
|
aa1ed9e233 | ||
|
|
3b6c199b1d | ||
|
|
6d22ceef24 | ||
|
|
54ff59737e | ||
|
|
516a37ba69 | ||
|
|
6a6bebd503 | ||
|
|
f9cbfa6d6c | ||
|
|
d4b3816c5d | ||
|
|
4a35d47515 | ||
|
|
52d4d61e2b | ||
|
|
5bff902394 | ||
|
|
f33eb86335 | ||
|
|
2ea26b9d1b | ||
|
|
10fa3e1619 | ||
|
|
956e17c81c | ||
|
|
6f749352d8 | ||
|
|
5f3f6c071b | ||
|
|
5795d43d0d | ||
|
|
de42f55e37 | ||
|
|
7e93a15204 | ||
|
|
715f819c3e | ||
|
|
345de8fb5d | ||
|
|
667a47c03a | ||
|
|
5dd5e304bd | ||
|
|
60a1589616 | ||
|
|
4d1d28672d | ||
|
|
fefca7d837 | ||
|
|
d9f5a573cd | ||
|
|
4c77cde9da | ||
|
|
658b098c3a | ||
|
|
70644ba31b | ||
|
|
06a706d070 | ||
|
|
14297986f2 | ||
|
|
0bd4d554d8 | ||
|
|
32c8d4f801 | ||
|
|
82abc20871 | ||
|
|
e8ab01f665 | ||
|
|
dee557a9ad | ||
|
|
f1fa2eba1d | ||
|
|
ccd58bbf23 | ||
|
|
54700d9cd0 | ||
|
|
984ea47f9d | ||
|
|
9458972d15 | ||
|
|
8eb9662694 | ||
|
|
a9be1aa608 | ||
|
|
3af9dff0ed | ||
|
|
c5e4b250b8 | ||
|
|
5fafa24974 | ||
|
|
b26c521930 | ||
|
|
eb52380ecf | ||
|
|
f1de69d7ff | ||
|
|
ba2214d8c7 | ||
|
|
fca4d7459c | ||
|
|
ad2338b898 | ||
|
|
c237f98044 | ||
|
|
87945ac506 | ||
|
|
11d96c894d | ||
|
|
8dcc16d640 | ||
|
|
9a1ddd0d41 | ||
|
|
b8bb58b083 | ||
|
|
7feba3c8c7 | ||
|
|
dae44c81d2 | ||
|
|
c2ac74a6be | ||
|
|
62811d92c5 | ||
|
|
feaee2bfe1 | ||
|
|
91701f609a | ||
|
|
d27aea2220 | ||
|
|
e832a57f35 | ||
|
|
13e6445498 | ||
|
|
552ce6b06d | ||
|
|
c3bce6730d | ||
|
|
5d2daedba3 | ||
|
|
6ee043449b | ||
|
|
5510d1e20d | ||
|
|
cad61dae37 | ||
|
|
1fcedca0f1 | ||
|
|
7b08225c57 | ||
|
|
06fd350e9c | ||
|
|
9ee747271a | ||
|
|
ef3c1ea0e7 |
57
.github/workflows/build.yml
vendored
Normal file
57
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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
|
||||||
96
.github/workflows/ci.yml
vendored
96
.github/workflows/ci.yml
vendored
@ -1,96 +0,0 @@
|
|||||||
name: signal-cli CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
pull_request:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write # to fetch code (actions/checkout) and submit dependency graph (gradle/gradle-build-action)
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
java: [ '21', '25' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: ${{ matrix.java }}
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
with:
|
|
||||||
dependency-graph: generate-and-submit
|
|
||||||
- name: Install asciidoc
|
|
||||||
run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: ./gradlew --no-daemon build
|
|
||||||
- name: Build man page
|
|
||||||
run: |
|
|
||||||
cd man
|
|
||||||
make install
|
|
||||||
- name: Add man page to archive
|
|
||||||
run: |
|
|
||||||
version=$(tar tf build/distributions/signal-cli-*.tar | head -n1 | sed 's|signal-cli-\([^/]*\)/.*|\1|')
|
|
||||||
echo $version
|
|
||||||
tar --transform="flags=r;s|man|signal-cli-${version}/man|" -rf build/distributions/signal-cli-${version}.tar man/man{1,5}
|
|
||||||
- name: Compress archive
|
|
||||||
run: gzip -n -9 build/distributions/signal-cli-*.tar
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: signal-cli-archive-${{ matrix.java }}
|
|
||||||
path: build/distributions/signal-cli-*.tar.gz
|
|
||||||
|
|
||||||
build-graalvm:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: graalvm/setup-graalvm@v1
|
|
||||||
with:
|
|
||||||
version: 'latest'
|
|
||||||
java-version: '21'
|
|
||||||
cache: 'gradle'
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: ./gradlew --no-daemon nativeCompile
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: signal-cli-native
|
|
||||||
path: build/native/nativeCompile/signal-cli
|
|
||||||
|
|
||||||
build-client:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- ubuntu
|
|
||||||
- macos
|
|
||||||
- windows
|
|
||||||
runs-on: ${{ matrix.os }}-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./client
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install rust
|
|
||||||
run: rustup default stable
|
|
||||||
- name: Build client
|
|
||||||
run: cargo build --release --verbose
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: signal-cli-client-${{ matrix.os }}
|
|
||||||
path: |
|
|
||||||
client/target/release/signal-cli-client
|
|
||||||
client/target/release/signal-cli-client.exe
|
|
||||||
12
.github/workflows/codeql-analysis.yml
vendored
12
.github/workflows/codeql-analysis.yml
vendored
@ -21,13 +21,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Setup Java JDK
|
- name: Setup Java JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: 21
|
java-version: 25
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
# with:
|
# with:
|
||||||
# languages: go, javascript, csharp, python, cpp, java
|
# languages: go, javascript, csharp, python, cpp, java
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@ -57,4 +57,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
|
|||||||
27
.github/workflows/dependency-graph.yml
vendored
Normal file
27
.github/workflows/dependency-graph.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
251
.github/workflows/release.yml
vendored
251
.github/workflows/release.yml
vendored
@ -5,197 +5,78 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: write # to fetch code (actions/checkout) and create release
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: signal-cli
|
IMAGE_NAME: signal-cli
|
||||||
IMAGE_REGISTRY: ghcr.io/asamk
|
IMAGE_REGISTRY: ghcr.io/asamk
|
||||||
REGISTRY_USER: ${{ github.actor }}
|
REGISTRY_USER: ${{ github.actor }}
|
||||||
REGISTRY_PASSWORD: ${{ github.token }}
|
REGISTRY_PASSWORD: ${{ github.token }}
|
||||||
|
ARCHIVE_JAVA_VERSION: 25
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: ./.github/workflows/build.yml
|
||||||
|
|
||||||
ci_wf:
|
release:
|
||||||
permissions:
|
needs: build
|
||||||
contents: write
|
|
||||||
uses: AsamK/signal-cli/.github/workflows/ci.yml@master
|
|
||||||
# ${{ github.repository }} not accepted here
|
|
||||||
|
|
||||||
lib_to_jar:
|
|
||||||
needs: ci_wf
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
signal_cli_version: ${{ steps.cli_ver.outputs.version }}
|
version: ${{ steps.version.outputs.version }}
|
||||||
release_id: ${{ steps.create_release.outputs.id }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
- name: Get signal-cli version
|
- name: Get signal-cli version
|
||||||
id: cli_ver
|
id: version
|
||||||
run: |
|
run: |
|
||||||
ver="${GITHUB_REF_NAME#v}"
|
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
||||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Extract archive
|
- name: Create draft release and upload assets
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
tree .
|
gh release create "v${{ steps.version.outputs.version }}" \
|
||||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
--repo "${{ github.repository }}" \
|
||||||
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
|
--title "v${{ steps.version.outputs.version }}" \
|
||||||
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
|
--draft \
|
||||||
rm -rf signal-cli-archive-*/
|
--verify-tag \
|
||||||
|
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
|
||||||
# - name: Get signal-client jar version
|
"signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \
|
||||||
# id: lib_ver
|
"signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \
|
||||||
# run: |
|
"signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz"
|
||||||
# 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 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 }}.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
# - name: Upload Linux archive
|
|
||||||
# uses: actions/upload-release-asset@v1
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
|
|
||||||
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
|
|
||||||
# asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload Linux native archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
# - name: Upload windows archive
|
|
||||||
# uses: actions/upload-release-asset@v1
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
|
|
||||||
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
|
|
||||||
# asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
#
|
|
||||||
# - name: Upload macos archive
|
|
||||||
# uses: actions/upload-release-asset@v1
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
|
|
||||||
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
|
|
||||||
# asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
build-container:
|
build-container:
|
||||||
needs: ci_wf
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
- name: Get signal-cli version
|
|
||||||
id: cli_ver
|
|
||||||
run: |
|
|
||||||
ver="${GITHUB_REF_NAME#v}"
|
|
||||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Move archive file
|
- name: Move archive file
|
||||||
run: |
|
run: |
|
||||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}.tar.gz
|
||||||
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
|
||||||
rm -r signal-cli-archive-* signal-cli-native
|
|
||||||
mkdir -p build/install/
|
mkdir -p build/install/
|
||||||
mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
|
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v3
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
|
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
||||||
containerfiles:
|
containerfiles: ./Containerfile
|
||||||
./Containerfile
|
|
||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v2
|
uses: redhat-actions/push-to-registry@v3
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
@ -209,41 +90,75 @@ jobs:
|
|||||||
echo "${{ toJSON(steps.push.outputs) }}"
|
echo "${{ toJSON(steps.push.outputs) }}"
|
||||||
|
|
||||||
build-container-native:
|
build-container-native:
|
||||||
needs: ci_wf
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
- name: Get signal-cli version
|
|
||||||
id: cli_ver
|
|
||||||
run: |
|
|
||||||
ver="${GITHUB_REF_NAME#v}"
|
|
||||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Move archive file
|
- name: Move archive file
|
||||||
run: |
|
run: |
|
||||||
|
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
|
||||||
mkdir -p build/native/nativeCompile/
|
mkdir -p build/native/nativeCompile/
|
||||||
chmod +x ./signal-cli-native/signal-cli
|
mv signal-cli build/native/nativeCompile/
|
||||||
mv ./signal-cli-native/signal-cli build/native/nativeCompile/
|
chmod +x build/native/nativeCompile/signal-cli
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v3
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
|
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
||||||
containerfiles:
|
containerfiles: ./native.Containerfile
|
||||||
./native.Containerfile
|
|
||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v2
|
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
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
.gradle/
|
.gradle/
|
||||||
|
.kotlin/
|
||||||
.idea/*
|
.idea/*
|
||||||
!.idea/codeStyles/
|
!.idea/codeStyles/
|
||||||
build/
|
build/
|
||||||
@ -13,3 +14,10 @@ out/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/bin/
|
/bin/
|
||||||
/test-config/
|
/test-config/
|
||||||
|
/dist/
|
||||||
|
/github/
|
||||||
|
man/*.1
|
||||||
|
man/*.5
|
||||||
|
man/man1
|
||||||
|
man/man5
|
||||||
|
.superpowers/
|
||||||
|
|||||||
182
CHANGELOG.md
182
CHANGELOG.md
@ -1,5 +1,183 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.14.7] - 2026-08-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Sync installed sticker packs via storage sync
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Set width/height for outgoing image attachments (Thanks @mikesimone)
|
||||||
|
- Update to new registration/provisioning API
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Removing self from pending group invite now works if invited via PNI
|
||||||
|
- Correctly set needs PNI signature for recipients
|
||||||
|
- Fix linking after previous unsuccessful link attempt
|
||||||
|
|
||||||
|
## [0.14.6] - 2026-07-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- New `sendStory` command to post file attachment stories to "My Story" or to a group via `--group-id`
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- The account parameter `-a` now supports ACI in addition to phone number
|
||||||
|
- Disabling read receipts in configuration now prevents sending read receipts (only sync message to linked devices is still sent)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Sending to large groups is no longer slowed down by members that are already known to be unregistered; they are skipped instead of being retried via the legacy 1:1 send path on every send.
|
||||||
|
|
||||||
|
## [0.14.5] - 2026-06-11
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Disable host validation when binding on 0.0.0.0
|
||||||
|
- Use new SVR2 enclave for PINs
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Receiving unidentified sender messages after signal server change
|
||||||
|
|
||||||
|
## [0.14.4] - 2026-05-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support for a global configuration file to set system-wide defaults
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Group admins can now see profile information for users requesting to join groups.
|
||||||
|
- Storage sync with unregistered contacts fixed
|
||||||
|
- Incoming messages are validated more accurately, fixing receiving messages from new contacts
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Some security and stability improvements, including HTTP HOST header validation and safer temporary file handling.
|
||||||
|
|
||||||
|
## [0.14.3] - 2026-04-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix sender key re-distribution on every group message (Thanks @meinecke)
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Performance improvement when assigning admin role to multiple group members
|
||||||
|
- Increase disconnect timeout for websocket connections
|
||||||
|
- Release builds are now reproducible
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Send message results now surface server-advised retry time for plain rate-limit (HTTP 413) failures, not only for proof-required challenges. The `retryAfterSeconds` field in JSON-RPC `SendMessageResult` is populated whenever the server sends a `Retry-After` header. The canonical way to distinguish proof-required failures remains `token != null`. Text output includes "retry after N seconds" when known.
|
||||||
|
- Add distinct JSON-RPC error code (6) for captcha rejection (Thanks @tonycpsu)
|
||||||
|
- No longer sends busy call response to allow linked devices to accept call
|
||||||
|
|
||||||
|
## [0.14.2] - 2026-04-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `--voice-note` parameter to `send` command (Thanks @Kevin)
|
||||||
|
- Add experimental support for voice calling (Thanks @visigoth)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix `updateGroup` command for adding admins and removing members (Thanks @joeykrim)
|
||||||
|
|
||||||
|
## [0.14.1] - 2026-03-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added isArchived to contact json output (Thanks @moppman)
|
||||||
|
- Added support for group member labels
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Adapt registration to signal server changes
|
||||||
|
|
||||||
|
## [0.14.0] - 2026-03-01
|
||||||
|
|
||||||
|
**Attention**: Now requires Java 25
|
||||||
|
|
||||||
|
Requires libsignal-client version 0.87.4.
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- Remove isRegistered method without parameters from Signal dbus interface, which always returned `true`
|
||||||
|
- Remove `sandbox` value for --service-environment parameter, use `staging` instead
|
||||||
|
- The `daemon` command now requires at least one channel parameter (`--socket`, `--dbus`, ...) and no longer defaults to
|
||||||
|
dbus
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The `link` command now prints a QR code in the terminal. (Thanks @karel1980)
|
||||||
|
- Add --ignore-avatars flag to prevent downloading avatars
|
||||||
|
- Add --ignore-stickers flag to prevent downloading sticker packs
|
||||||
|
- Add `--no-urgent` flag to `send` command to send messages that don't trigger a push notification. (Thanks @kaikozlov)
|
||||||
|
- Add `sendPinMessage`/`sendUnpinMessage` commands for pinning messages
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Improved behavior for unregistered contacts
|
||||||
|
- Profiles are refreshed when using the listContacts command
|
||||||
|
- For long text messages the text attachment is used instead of the truncated body
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Adapted to new binary aci/pni formats in the Signal protocol
|
||||||
|
- Group invites should now work when the user was invited via phone number (PNI only)
|
||||||
|
|
||||||
|
## [0.13.24] - 2026-02-05
|
||||||
|
|
||||||
|
Requires libsignal-client version 0.87.0.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Improve performance of first send to large group
|
||||||
|
- Improve envelope validation
|
||||||
|
|
||||||
|
## [0.13.23] - 2026-01-24
|
||||||
|
|
||||||
|
Requires libsignal-client version 0.86.12.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add sendPollCreate, sendPollVote, sendPollTerminate commands for polls
|
||||||
|
- Add updateDevice command to set device name of linked devices
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Allow updating contact names from linked devices
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Start multi account mode even if some accounts have authorization failures
|
||||||
|
|
||||||
|
## [0.13.22] - 2025-11-14
|
||||||
|
|
||||||
|
Requires libsignal-client version 0.86.1.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix timeout handling for receive command
|
||||||
|
- Fix device link URI parsing for unencoded trailing =
|
||||||
|
- Adapt setPin command to server changes
|
||||||
|
|
||||||
|
## [0.13.21] - 2025-10-25
|
||||||
|
|
||||||
|
Requires libsignal-client version 0.84.0.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Add isExpirationUpdate to json message output
|
||||||
|
- Improve error message when using verify without registering before
|
||||||
|
|
||||||
## [0.13.20] - 2025-09-23
|
## [0.13.20] - 2025-09-23
|
||||||
|
|
||||||
Requires libsignal-client version 0.81.0.
|
Requires libsignal-client version 0.81.0.
|
||||||
@ -101,9 +279,11 @@ Requires libsignal-client version 0.68.1.
|
|||||||
Requires libsignal-client version 0.66.2.
|
Requires libsignal-client version 0.66.2.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Allow setting nickname and note with `updateContact` command
|
- Allow setting nickname and note with `updateContact` command
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix syncing nickname, note and expiration timer
|
- Fix syncing nickname, note and expiration timer
|
||||||
- Fix check for registered users with a proxy
|
- Fix check for registered users with a proxy
|
||||||
- Improve handling of storage records not yet supported by signal-cli
|
- Improve handling of storage records not yet supported by signal-cli
|
||||||
@ -129,6 +309,7 @@ Requires libsignal-client version 0.65.2.
|
|||||||
Requires libsignal-client version 0.64.0.
|
Requires libsignal-client version 0.64.0.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix issue with receiving messages that have an invalid destination
|
- Fix issue with receiving messages that have an invalid destination
|
||||||
|
|
||||||
## [0.13.10] - 2024-11-30
|
## [0.13.10] - 2024-11-30
|
||||||
@ -141,6 +322,7 @@ Requires libsignal-client version 0.62.0.
|
|||||||
- Fix receiving expiration timer updates
|
- Fix receiving expiration timer updates
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
|
|
||||||
- Add support for new storage encryption scheme
|
- Add support for new storage encryption scheme
|
||||||
|
|
||||||
## [0.13.9] - 2024-10-28
|
## [0.13.9] - 2024-10-28
|
||||||
|
|||||||
@ -10,7 +10,7 @@ If you have a question you can ask it in the [GitHub discussions page](https://g
|
|||||||
- Be sure to include a **title and clear description**, as much relevant information as possible.
|
- Be sure to include a **title and clear description**, as much relevant information as possible.
|
||||||
- Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using
|
- Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using
|
||||||
- Specify if it's the normal java or the graalvm native version.
|
- Specify if it's the normal java or the graalvm native version.
|
||||||
- Run the failing command with `--verbose` flag to get a more detailed log output and include that in the bug report
|
- Run the failing command with `-vv --scrub-log` flags to get a more detailed log output and include that in the bug report
|
||||||
|
|
||||||
# Pull request
|
# Pull request
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM docker.io/azul/zulu-openjdk:21-jre-headless
|
FROM docker.io/azul/zulu-openjdk:25-jre-headless
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
|
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
|
||||||
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
|
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
|
||||||
|
|||||||
43
README.md
43
README.md
@ -3,12 +3,12 @@
|
|||||||
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
||||||
It supports registering, verifying, sending and receiving messages.
|
It supports registering, verifying, sending and receiving messages.
|
||||||
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
||||||
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal-service).
|
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/lib/libsignal-service).
|
||||||
For registering you need a phone number where you can receive SMS or incoming calls.
|
For registering you need a phone number where you can receive SMS or incoming calls.
|
||||||
|
|
||||||
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
||||||
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))
|
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)) .
|
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)).
|
||||||
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
|
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
|
||||||
|
|
||||||
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
|
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
|
||||||
@ -23,24 +23,33 @@ Windows. There's also a [docker image and some Linux packages](https://github.co
|
|||||||
|
|
||||||
System requirements:
|
System requirements:
|
||||||
|
|
||||||
- at least Java Runtime Environment (JRE) 21
|
- at least Java Runtime Environment (JRE) 25
|
||||||
- native library: libsignal-client
|
- native library: libsignal-client
|
||||||
|
|
||||||
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
|
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
|
||||||
systems/architectures
|
systems/architectures
|
||||||
see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
|
see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
|
||||||
|
|
||||||
### Install system-wide on Linux
|
### Install system-wide on Linux [ JVM build ]
|
||||||
|
|
||||||
See [latest version](https://github.com/AsamK/signal-cli/releases).
|
See [latest version](https://github.com/AsamK/signal-cli/releases).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export VERSION=<latest version, format "x.y.z">
|
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
|
||||||
wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz
|
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
|
sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt
|
||||||
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
|
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Install system-wide on Linux [ GraalVM native build ]
|
||||||
|
|
||||||
|
```sh
|
||||||
|
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
|
||||||
|
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux-native.tar.gz
|
||||||
|
sudo tar xf signal-cli-"${VERSION}"-Linux-native.tar.gz -C /opt
|
||||||
|
sudo ln -sf /opt/signal-cli /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
You can find further instructions on the Wiki:
|
You can find further instructions on the Wiki:
|
||||||
|
|
||||||
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
|
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
|
||||||
@ -55,8 +64,20 @@ Important: The ACCOUNT is your phone number in international format and must inc
|
|||||||
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
|
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
|
||||||
of all country codes.)
|
of all country codes.)
|
||||||
|
|
||||||
|
* Link to an existing account
|
||||||
|
|
||||||
|
If you have an existing Signal account associated
|
||||||
|
with a number, you can link signal-cli to it with:
|
||||||
|
|
||||||
|
signal-cli link
|
||||||
|
|
||||||
* Register a number (with SMS verification)
|
* Register a number (with SMS verification)
|
||||||
|
|
||||||
|
Alternatively, if you don't have an existing Signal
|
||||||
|
account, you can register one from signal-cli. Note
|
||||||
|
that this will unregister any existing client
|
||||||
|
associated with the same number.
|
||||||
|
|
||||||
signal-cli -a ACCOUNT register
|
signal-cli -a ACCOUNT register
|
||||||
|
|
||||||
You can register Signal using a landline number. In this case, you need to follow the procedure below:
|
You can register Signal using a landline number. In this case, you need to follow the procedure below:
|
||||||
@ -139,6 +160,16 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
|
|||||||
./gradlew run --args="--help"
|
./gradlew run --args="--help"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### JSON Schemas for the JSON-RPC mode
|
||||||
|
|
||||||
|
1. Generate [JSON Schema](https://json-schema.org/) files for all the JSON-RPC data classes (`src/main/java/org/asamk/signal/json`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew jsonSchemas
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The generated files can be found in the `build/generated/META-INF/schemas` folder.
|
||||||
|
|
||||||
### Building a native binary with GraalVM (EXPERIMENTAL)
|
### Building a native binary with GraalVM (EXPERIMENTAL)
|
||||||
|
|
||||||
It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not
|
It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
|
import groovy.json.JsonOutput
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
application
|
application
|
||||||
eclipse
|
eclipse
|
||||||
`check-lib-versions`
|
`check-lib-versions`
|
||||||
id("org.graalvm.buildtools.native") version "0.11.0"
|
id("org.graalvm.buildtools.native") version "1.1.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "org.asamk"
|
group = "org.asamk"
|
||||||
version = "0.13.20"
|
version = "0.14.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_25
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_25
|
||||||
|
|
||||||
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
||||||
toolchain {
|
toolchain {
|
||||||
@ -30,16 +32,15 @@ application {
|
|||||||
graalvmNative {
|
graalvmNative {
|
||||||
binaries {
|
binaries {
|
||||||
this["main"].run {
|
this["main"].run {
|
||||||
buildArgs.add("--install-exit-handlers")
|
|
||||||
buildArgs.add("-Dfile.encoding=UTF-8")
|
buildArgs.add("-Dfile.encoding=UTF-8")
|
||||||
buildArgs.add("-J-Dfile.encoding=UTF-8")
|
buildArgs.add("-J-Dfile.encoding=UTF-8")
|
||||||
buildArgs.add("-march=compatibility")
|
buildArgs.add("-march=compatibility")
|
||||||
|
buildArgs.add("--enable-native-access=ALL-UNNAMED")
|
||||||
resources.autodetect()
|
resources.autodetect()
|
||||||
configurationFileDirectories.from(file("graalvm-config-dir"))
|
|
||||||
if (System.getenv("GRAALVM_HOME") == null) {
|
if (System.getenv("GRAALVM_HOME") == null) {
|
||||||
toolchainDetection.set(true)
|
toolchainDetection.set(true)
|
||||||
javaLauncher.set(javaToolchains.launcherFor {
|
javaLauncher.set(javaToolchains.launcherFor {
|
||||||
languageVersion.set(JavaLanguageVersion.of(21))
|
languageVersion.set(JavaLanguageVersion.of(25))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
toolchainDetection.set(false)
|
toolchainDetection.set(false)
|
||||||
@ -73,6 +74,11 @@ val excludePatterns = mapOf(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val schemaAnnotationProcessor = configurations.create("schemaAnnotationProcessor") {
|
||||||
|
isCanBeConsumed = false
|
||||||
|
isCanBeResolved = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
registerTransform(JarFileExcluder::class) {
|
registerTransform(JarFileExcluder::class) {
|
||||||
from.attribute(minified, false).attribute(artifactType, "jar")
|
from.attribute(minified, false).attribute(artifactType, "jar")
|
||||||
@ -83,6 +89,8 @@ dependencies {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schemaAnnotationProcessor(libs.micronaut.json.schema.processor)
|
||||||
|
schemaAnnotationProcessor(libs.micronaut.inject.java)
|
||||||
implementation(libs.bouncycastle)
|
implementation(libs.bouncycastle)
|
||||||
implementation(libs.jackson.databind)
|
implementation(libs.jackson.databind)
|
||||||
implementation(libs.argparse4j)
|
implementation(libs.argparse4j)
|
||||||
@ -90,7 +98,20 @@ dependencies {
|
|||||||
implementation(libs.slf4j.api)
|
implementation(libs.slf4j.api)
|
||||||
implementation(libs.slf4j.jul)
|
implementation(libs.slf4j.jul)
|
||||||
implementation(libs.logback)
|
implementation(libs.logback)
|
||||||
|
implementation(libs.zxing)
|
||||||
|
implementation(libs.micronaut.json.schema.annotations)
|
||||||
|
if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) {
|
||||||
|
implementation(libs.micronaut.json.schema.generator)
|
||||||
|
}
|
||||||
implementation(project(":libsignal-cli"))
|
implementation(project(":libsignal-cli"))
|
||||||
|
|
||||||
|
testImplementation(libs.junit.jupiter)
|
||||||
|
testImplementation(platform(libs.junit.jupiter.bom))
|
||||||
|
testRuntimeOnly(libs.junit.launcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<Test>("test") {
|
||||||
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
@ -124,6 +145,7 @@ tasks.register("fatJar", type = Jar::class) {
|
|||||||
archiveBaseName.set("${project.name}-fat")
|
archiveBaseName.set("${project.name}-fat")
|
||||||
exclude(
|
exclude(
|
||||||
"META-INF/*.SF",
|
"META-INF/*.SF",
|
||||||
|
"META-INF/**/*.MF",
|
||||||
"META-INF/*.DSA",
|
"META-INF/*.DSA",
|
||||||
"META-INF/*.RSA",
|
"META-INF/*.RSA",
|
||||||
"META-INF/NOTICE*",
|
"META-INF/NOTICE*",
|
||||||
@ -137,3 +159,44 @@ tasks.register("fatJar", type = Jar::class) {
|
|||||||
}
|
}
|
||||||
with(tasks.jar.get())
|
with(tasks.jar.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register("writeLibsignalVersion") {
|
||||||
|
doLast {
|
||||||
|
val resolutionResult = configurations.runtimeClasspath.get().incoming.resolutionResult
|
||||||
|
val libsignalDep =
|
||||||
|
resolutionResult.allDependencies.find { dep -> dep.requested is ModuleComponentSelector && (dep.requested as ModuleComponentSelector).group == "org.signal" && (dep.requested as ModuleComponentSelector).moduleIdentifier.name == "libsignal-client" }
|
||||||
|
if (libsignalDep != null) {
|
||||||
|
val version = (libsignalDep.requested as ModuleComponentSelector).version
|
||||||
|
file("libsignal-version").writeText(version + "\n")
|
||||||
|
} else {
|
||||||
|
throw GradleException("Could not find libsignal-client dependency")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<JavaCompile>("jsonSchemas") {
|
||||||
|
dependsOn(tasks.compileJava)
|
||||||
|
val schemaBaseUri = "http://localhost:8080/schemas/"
|
||||||
|
source = sourceSets.main.get().java
|
||||||
|
include("org/asamk/signal/json/**/*.java")
|
||||||
|
classpath = sourceSets.main.get().compileClasspath + files(sourceSets.main.get().java.destinationDirectory)
|
||||||
|
destinationDirectory.set(layout.buildDirectory.dir("generated"))
|
||||||
|
options.annotationProcessorPath = schemaAnnotationProcessor
|
||||||
|
options.compilerArgs.addAll(
|
||||||
|
listOf(
|
||||||
|
"-Amicronaut.processing.group=org.asamk",
|
||||||
|
"-Amicronaut.processing.module=signal-cli",
|
||||||
|
"-Amicronaut.processing.annotations=org.asamk.signal.json.*",
|
||||||
|
"-Amicronaut.jsonschema.baseUri=$schemaBaseUri",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
doLast {
|
||||||
|
fileTree(destinationDirectory.get().dir("META-INF/schemas").asFile) {
|
||||||
|
include("*.schema.json")
|
||||||
|
}.forEach { schemaFile ->
|
||||||
|
val normalized = schemaFile.readText().replace("\"$schemaBaseUri/", "\"")
|
||||||
|
val prettyJson = JsonOutput.prettyPrint(normalized)
|
||||||
|
schemaFile.writeText("$prettyJson\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,11 +7,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
|
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
|
||||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
|
compilerOptions.jvmTarget.set(JvmTarget.JVM_25)
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_25
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.Task
|
import org.gradle.api.Task
|
||||||
@ -8,7 +6,7 @@ import javax.xml.parsers.DocumentBuilderFactory
|
|||||||
|
|
||||||
class CheckLibVersionsPlugin : Plugin<Project> {
|
class CheckLibVersionsPlugin : Plugin<Project> {
|
||||||
override fun apply(project: Project) {
|
override fun apply(project: Project) {
|
||||||
project.task("checkLibVersions") {
|
project.tasks.register("checkLibVersions") {
|
||||||
description =
|
description =
|
||||||
"Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded."
|
"Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded."
|
||||||
doLast {
|
doLast {
|
||||||
@ -28,7 +26,7 @@ class CheckLibVersionsPlugin : Plugin<Project> {
|
|||||||
try {
|
try {
|
||||||
val dbf = DocumentBuilderFactory.newInstance()
|
val dbf = DocumentBuilderFactory.newInstance()
|
||||||
val db = dbf.newDocumentBuilder()
|
val db = dbf.newDocumentBuilder()
|
||||||
val doc = db.parse(metaDataUrl);
|
val doc = db.parse(metaDataUrl)
|
||||||
val newest = doc.getElementsByTagName("latest").item(0).textContent
|
val newest = doc.getElementsByTagName("latest").item(0).textContent
|
||||||
if (version != newest.toString()) {
|
if (version != newest.toString()) {
|
||||||
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
||||||
|
|||||||
11
client.Containerfile
Normal file
11
client.Containerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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"]
|
||||||
671
client/Cargo.lock
generated
671
client/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,9 @@ pub struct Cli {
|
|||||||
#[arg(short = 'a', long)]
|
#[arg(short = 'a', long)]
|
||||||
pub account: Option<String>,
|
pub account: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub output: Option<String>,
|
||||||
|
|
||||||
/// TCP host and port of signal-cli daemon
|
/// TCP host and port of signal-cli daemon
|
||||||
#[arg(long, conflicts_with = "json_rpc_http")]
|
#[arg(long, conflicts_with = "json_rpc_http")]
|
||||||
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
||||||
@ -94,7 +97,7 @@ pub enum CliCommands {
|
|||||||
},
|
},
|
||||||
Link {
|
Link {
|
||||||
#[arg(short = 'n', long)]
|
#[arg(short = 'n', long)]
|
||||||
name: String,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
ListAccounts,
|
ListAccounts,
|
||||||
ListContacts {
|
ListContacts {
|
||||||
@ -105,6 +108,10 @@ pub enum CliCommands {
|
|||||||
blocked: Option<bool>,
|
blocked: Option<bool>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
detailed: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
internal: bool,
|
||||||
},
|
},
|
||||||
ListDevices,
|
ListDevices,
|
||||||
ListGroups {
|
ListGroups {
|
||||||
@ -135,6 +142,8 @@ pub enum CliCommands {
|
|||||||
voice: bool,
|
voice: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
captcha: Option<String>,
|
captcha: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
reregister: bool,
|
||||||
},
|
},
|
||||||
RemoveContact {
|
RemoveContact {
|
||||||
recipient: String,
|
recipient: String,
|
||||||
@ -167,15 +176,24 @@ pub enum CliCommands {
|
|||||||
#[arg(short = 'g', long)]
|
#[arg(short = 'g', long)]
|
||||||
group_id: Vec<String>,
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
note_to_self: bool,
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
|
||||||
#[arg(short = 'e', long)]
|
#[arg(short = 'e', long)]
|
||||||
end_session: bool,
|
end_session: bool,
|
||||||
|
|
||||||
#[arg(short = 'm', long)]
|
#[arg(short = 'm', long)]
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
message_from_stdin: bool,
|
||||||
|
|
||||||
#[arg(short = 'a', long)]
|
#[arg(short = 'a', long)]
|
||||||
attachment: Vec<String>,
|
attachment: Vec<String>,
|
||||||
|
|
||||||
@ -229,6 +247,25 @@ pub enum CliCommands {
|
|||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
edit_timestamp: Option<u64>,
|
edit_timestamp: Option<u64>,
|
||||||
|
|
||||||
|
#[arg(long = "no-urgent")]
|
||||||
|
no_urgent: bool,
|
||||||
|
},
|
||||||
|
SendAdminDelete {
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'a', long = "target-author")]
|
||||||
|
target_author: String,
|
||||||
|
|
||||||
|
#[arg(short = 't', long = "target-timestamp")]
|
||||||
|
target_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
story: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
},
|
},
|
||||||
SendContacts,
|
SendContacts,
|
||||||
SendPaymentNotification {
|
SendPaymentNotification {
|
||||||
@ -240,15 +277,117 @@ pub enum CliCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
note: String,
|
note: String,
|
||||||
},
|
},
|
||||||
|
SendPinMessage {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'a', long = "target-author")]
|
||||||
|
target_author: String,
|
||||||
|
|
||||||
|
#[arg(short = 't', long = "target-timestamp")]
|
||||||
|
target_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(short = 'd', long = "pin-duration")]
|
||||||
|
pin_duration: Option<i32>,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
story: bool,
|
||||||
|
},
|
||||||
|
SendPollCreate {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'q', long = "question")]
|
||||||
|
question: String,
|
||||||
|
|
||||||
|
#[arg(short = 'o', long = "option")]
|
||||||
|
option: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long = "no-multi")]
|
||||||
|
no_multi: bool,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
},
|
||||||
|
SendPollTerminate {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long = "poll-timestamp")]
|
||||||
|
poll_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
},
|
||||||
|
SendPollVote {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long = "poll-author")]
|
||||||
|
poll_author: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "poll-timestamp")]
|
||||||
|
poll_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(short = 'o', long = "option")]
|
||||||
|
option: Vec<i32>,
|
||||||
|
|
||||||
|
#[arg(long = "vote-count")]
|
||||||
|
vote_count: i32,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
},
|
||||||
SendReaction {
|
SendReaction {
|
||||||
recipient: Vec<String>,
|
recipient: Vec<String>,
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
#[arg(short = 'g', long = "group-id")]
|
||||||
group_id: Vec<String>,
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
#[arg(long = "note-to-self")]
|
||||||
note_to_self: bool,
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
|
||||||
#[arg(short = 'e', long)]
|
#[arg(short = 'e', long)]
|
||||||
emoji: String,
|
emoji: String,
|
||||||
|
|
||||||
@ -267,6 +406,9 @@ pub enum CliCommands {
|
|||||||
SendReceipt {
|
SendReceipt {
|
||||||
recipient: String,
|
recipient: String,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
#[arg(short = 't', long = "target-timestamp")]
|
#[arg(short = 't', long = "target-timestamp")]
|
||||||
target_timestamp: Vec<u64>,
|
target_timestamp: Vec<u64>,
|
||||||
|
|
||||||
@ -283,12 +425,37 @@ pub enum CliCommands {
|
|||||||
#[arg(short = 's', long)]
|
#[arg(short = 's', long)]
|
||||||
stop: bool,
|
stop: bool,
|
||||||
},
|
},
|
||||||
|
SendUnpinMessage {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'a', long = "target-author")]
|
||||||
|
target_author: String,
|
||||||
|
|
||||||
|
#[arg(short = 't', long = "target-timestamp")]
|
||||||
|
target_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
story: bool,
|
||||||
|
},
|
||||||
SendMessageRequestResponse {
|
SendMessageRequestResponse {
|
||||||
recipient: Vec<String>,
|
recipient: Vec<String>,
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
#[arg(short = 'g', long = "group-id")]
|
||||||
group_id: Vec<String>,
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
r#type: MessageRequestResponseType,
|
r#type: MessageRequestResponseType,
|
||||||
},
|
},
|
||||||
SetPin {
|
SetPin {
|
||||||
@ -334,6 +501,10 @@ pub enum CliCommands {
|
|||||||
discoverable_by_number: Option<bool>,
|
discoverable_by_number: Option<bool>,
|
||||||
#[arg(long = "number-sharing")]
|
#[arg(long = "number-sharing")]
|
||||||
number_sharing: Option<bool>,
|
number_sharing: Option<bool>,
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Option<String>,
|
||||||
|
#[arg(long = "delete-username")]
|
||||||
|
delete_username: bool,
|
||||||
},
|
},
|
||||||
UpdateConfiguration {
|
UpdateConfiguration {
|
||||||
#[arg(long = "read-receipts")]
|
#[arg(long = "read-receipts")]
|
||||||
@ -356,6 +527,28 @@ pub enum CliCommands {
|
|||||||
|
|
||||||
#[arg(short = 'n', long)]
|
#[arg(short = 'n', long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "given-name")]
|
||||||
|
given_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "family-name")]
|
||||||
|
family_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "nick-given-name")]
|
||||||
|
nick_given_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "nick-family-name")]
|
||||||
|
nick_family_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
note: Option<String>,
|
||||||
|
},
|
||||||
|
UpdateDevice {
|
||||||
|
#[arg(short = 'd', long = "device-id")]
|
||||||
|
device_id: u32,
|
||||||
|
|
||||||
|
#[arg(short = 'n', long = "device-name")]
|
||||||
|
device_name: String,
|
||||||
},
|
},
|
||||||
UpdateGroup {
|
UpdateGroup {
|
||||||
#[arg(short = 'g', long = "group-id")]
|
#[arg(short = 'g', long = "group-id")]
|
||||||
@ -405,6 +598,12 @@ pub enum CliCommands {
|
|||||||
|
|
||||||
#[arg(short = 'e', long)]
|
#[arg(short = 'e', long)]
|
||||||
expiration: Option<u32>,
|
expiration: Option<u32>,
|
||||||
|
|
||||||
|
#[arg(long = "member-label-emoji")]
|
||||||
|
member_label_emoji: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "member-label")]
|
||||||
|
member_label: Option<String>,
|
||||||
},
|
},
|
||||||
UpdateProfile {
|
UpdateProfile {
|
||||||
#[arg(long = "given-name")]
|
#[arg(long = "given-name")]
|
||||||
|
|||||||
@ -90,7 +90,7 @@ pub trait Rpc {
|
|||||||
fn finish_link(
|
fn finish_link(
|
||||||
&self,
|
&self,
|
||||||
#[allow(non_snake_case)] deviceLinkUri: String,
|
#[allow(non_snake_case)] deviceLinkUri: String,
|
||||||
#[allow(non_snake_case)] deviceName: String,
|
#[allow(non_snake_case)] deviceName: Option<String>,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "listAccounts", param_kind = map)]
|
#[method(name = "listAccounts", param_kind = map)]
|
||||||
@ -104,6 +104,8 @@ pub trait Rpc {
|
|||||||
#[allow(non_snake_case)] allRecipients: bool,
|
#[allow(non_snake_case)] allRecipients: bool,
|
||||||
blocked: Option<bool>,
|
blocked: Option<bool>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
detailed: bool,
|
||||||
|
internal: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "listDevices", param_kind = map)]
|
#[method(name = "listDevices", param_kind = map)]
|
||||||
@ -141,6 +143,7 @@ pub trait Rpc {
|
|||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
voice: bool,
|
voice: bool,
|
||||||
captcha: Option<String>,
|
captcha: Option<String>,
|
||||||
|
reregister: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "removeContact", param_kind = map)]
|
#[method(name = "removeContact", param_kind = map)]
|
||||||
@ -179,32 +182,116 @@ pub trait Rpc {
|
|||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
groupIds: Vec<String>,
|
groupIds: Vec<String>,
|
||||||
noteToSelf: bool,
|
usernames: Vec<String>,
|
||||||
endSession: bool,
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] endSession: bool,
|
||||||
message: String,
|
message: String,
|
||||||
attachments: Vec<String>,
|
attachments: Vec<String>,
|
||||||
viewOnce: bool,
|
#[allow(non_snake_case)] viewOnce: bool,
|
||||||
mentions: Vec<String>,
|
mentions: Vec<String>,
|
||||||
textStyle: Vec<String>,
|
#[allow(non_snake_case)] textStyle: Vec<String>,
|
||||||
quoteTimestamp: Option<u64>,
|
#[allow(non_snake_case)] quoteTimestamp: Option<u64>,
|
||||||
quoteAuthor: Option<String>,
|
#[allow(non_snake_case)] quoteAuthor: Option<String>,
|
||||||
quoteMessage: Option<String>,
|
#[allow(non_snake_case)] quoteMessage: Option<String>,
|
||||||
quoteMention: Vec<String>,
|
#[allow(non_snake_case)] quoteMention: Vec<String>,
|
||||||
quoteTextStyle: Vec<String>,
|
#[allow(non_snake_case)] quoteTextStyle: Vec<String>,
|
||||||
quoteAttachment: Vec<String>,
|
#[allow(non_snake_case)] quoteAttachment: Vec<String>,
|
||||||
previewUrl: Option<String>,
|
#[allow(non_snake_case)] previewUrl: Option<String>,
|
||||||
previewTitle: Option<String>,
|
#[allow(non_snake_case)] previewTitle: Option<String>,
|
||||||
previewDescription: Option<String>,
|
#[allow(non_snake_case)] previewDescription: Option<String>,
|
||||||
previewImage: Option<String>,
|
#[allow(non_snake_case)] previewImage: Option<String>,
|
||||||
sticker: Option<String>,
|
sticker: Option<String>,
|
||||||
storyTimestamp: Option<u64>,
|
#[allow(non_snake_case)] storyTimestamp: Option<u64>,
|
||||||
storyAuthor: Option<String>,
|
#[allow(non_snake_case)] storyAuthor: Option<String>,
|
||||||
editTimestamp: Option<u64>,
|
#[allow(non_snake_case)] editTimestamp: Option<u64>,
|
||||||
|
#[allow(non_snake_case)] noUrgent: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "sendContacts", param_kind = map)]
|
#[method(name = "sendContacts", param_kind = map)]
|
||||||
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendAdminDelete", param_kind = map)]
|
||||||
|
fn send_admin_delete(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] targetAuthor: String,
|
||||||
|
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||||
|
story: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendPinMessage", param_kind = map)]
|
||||||
|
fn send_pin_message(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] targetAuthor: String,
|
||||||
|
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||||
|
#[allow(non_snake_case)] pinDuration: Option<i32>,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
story: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendPollCreate", param_kind = map)]
|
||||||
|
fn send_poll_create(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
question: String,
|
||||||
|
option: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] noMulti: bool,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendPollVote", param_kind = map)]
|
||||||
|
fn send_poll_vote(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] pollAuthor: Option<String>,
|
||||||
|
#[allow(non_snake_case)] pollTimestamp: u64,
|
||||||
|
option: Vec<i32>,
|
||||||
|
#[allow(non_snake_case)] voteCount: i32,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendPollTerminate", param_kind = map)]
|
||||||
|
fn send_poll_terminate(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] pollTimestamp: u64,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendUnpinMessage", param_kind = map)]
|
||||||
|
fn send_unpin_message(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] targetAuthor: String,
|
||||||
|
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
story: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "sendPaymentNotification", param_kind = map)]
|
#[method(name = "sendPaymentNotification", param_kind = map)]
|
||||||
fn send_payment_notification(
|
fn send_payment_notification(
|
||||||
&self,
|
&self,
|
||||||
@ -220,7 +307,9 @@ pub trait Rpc {
|
|||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
emoji: String,
|
emoji: String,
|
||||||
#[allow(non_snake_case)] targetAuthor: String,
|
#[allow(non_snake_case)] targetAuthor: String,
|
||||||
#[allow(non_snake_case)] targetTimestamp: u64,
|
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||||
@ -233,6 +322,7 @@ pub trait Rpc {
|
|||||||
&self,
|
&self,
|
||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipient: String,
|
recipient: String,
|
||||||
|
usernames: Vec<String>,
|
||||||
#[allow(non_snake_case)] targetTimestamps: Vec<u64>,
|
#[allow(non_snake_case)] targetTimestamps: Vec<u64>,
|
||||||
r#type: String,
|
r#type: String,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
@ -314,6 +404,8 @@ pub trait Rpc {
|
|||||||
unrestrictedUnidentifiedSender: Option<bool>,
|
unrestrictedUnidentifiedSender: Option<bool>,
|
||||||
discoverableByNumber: Option<bool>,
|
discoverableByNumber: Option<bool>,
|
||||||
numberSharing: Option<bool>,
|
numberSharing: Option<bool>,
|
||||||
|
username: Option<String>,
|
||||||
|
deleteUsername: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "updateConfiguration", param_kind = map)]
|
#[method(name = "updateConfiguration", param_kind = map)]
|
||||||
@ -333,6 +425,19 @@ pub trait Rpc {
|
|||||||
recipient: String,
|
recipient: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
expiration: Option<u32>,
|
expiration: Option<u32>,
|
||||||
|
#[allow(non_snake_case)] givenName: Option<String>,
|
||||||
|
#[allow(non_snake_case)] familyName: Option<String>,
|
||||||
|
#[allow(non_snake_case)] nickGivenName: Option<String>,
|
||||||
|
#[allow(non_snake_case)] nickFamilyName: Option<String>,
|
||||||
|
note: Option<String>,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "updateDevice", param_kind = map)]
|
||||||
|
fn update_device(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
#[allow(non_snake_case)] deviceId: u32,
|
||||||
|
#[allow(non_snake_case)] deviceName: String,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "updateGroup", param_kind = map)]
|
#[method(name = "updateGroup", param_kind = map)]
|
||||||
@ -355,6 +460,8 @@ pub trait Rpc {
|
|||||||
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
|
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
|
||||||
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
|
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
|
||||||
expiration: Option<u32>,
|
expiration: Option<u32>,
|
||||||
|
#[allow(non_snake_case)] memberLabelEmoji: Option<String>,
|
||||||
|
#[allow(non_snake_case)] memberLabel: Option<String>,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "updateProfile", param_kind = map)]
|
#[method(name = "updateProfile", param_kind = map)]
|
||||||
|
|||||||
@ -84,9 +84,19 @@ async fn handle_command(
|
|||||||
all_recipients,
|
all_recipients,
|
||||||
blocked,
|
blocked,
|
||||||
name,
|
name,
|
||||||
|
detailed,
|
||||||
|
internal,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.list_contacts(cli.account, recipient, all_recipients, blocked, name)
|
.list_contacts(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
all_recipients,
|
||||||
|
blocked,
|
||||||
|
name,
|
||||||
|
detailed,
|
||||||
|
internal,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
CliCommands::ListDevices => client.list_devices(cli.account).await,
|
CliCommands::ListDevices => client.list_devices(cli.account).await,
|
||||||
@ -105,8 +115,14 @@ async fn handle_command(
|
|||||||
.quit_group(cli.account, group_id, delete, admin)
|
.quit_group(cli.account, group_id, delete, admin)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
CliCommands::Register { voice, captcha } => {
|
CliCommands::Register {
|
||||||
client.register(cli.account, voice, captcha).await
|
voice,
|
||||||
|
captcha,
|
||||||
|
reregister,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.register(cli.account, voice, captcha, reregister)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
CliCommands::RemoveContact {
|
CliCommands::RemoveContact {
|
||||||
recipient,
|
recipient,
|
||||||
@ -140,9 +156,12 @@ async fn handle_command(
|
|||||||
CliCommands::Send {
|
CliCommands::Send {
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
|
username,
|
||||||
|
notify_self,
|
||||||
note_to_self,
|
note_to_self,
|
||||||
end_session,
|
end_session,
|
||||||
message,
|
message,
|
||||||
|
message_from_stdin,
|
||||||
attachment,
|
attachment,
|
||||||
view_once,
|
view_once,
|
||||||
mention,
|
mention,
|
||||||
@ -161,15 +180,22 @@ async fn handle_command(
|
|||||||
story_timestamp,
|
story_timestamp,
|
||||||
story_author,
|
story_author,
|
||||||
edit_timestamp,
|
edit_timestamp,
|
||||||
|
no_urgent,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.send(
|
.send(
|
||||||
cli.account,
|
cli.account,
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
|
username,
|
||||||
|
notify_self,
|
||||||
note_to_self,
|
note_to_self,
|
||||||
end_session,
|
end_session,
|
||||||
message.unwrap_or_default(),
|
if message_from_stdin {
|
||||||
|
std::io::read_to_string(std::io::stdin()).unwrap()
|
||||||
|
} else {
|
||||||
|
message.unwrap_or_default()
|
||||||
|
},
|
||||||
attachment,
|
attachment,
|
||||||
view_once,
|
view_once,
|
||||||
mention,
|
mention,
|
||||||
@ -188,10 +214,29 @@ async fn handle_command(
|
|||||||
story_timestamp,
|
story_timestamp,
|
||||||
story_author,
|
story_author,
|
||||||
edit_timestamp,
|
edit_timestamp,
|
||||||
|
no_urgent,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
CliCommands::SendContacts => client.send_contacts(cli.account).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 {
|
CliCommands::SendPaymentNotification {
|
||||||
recipient,
|
recipient,
|
||||||
receipt,
|
receipt,
|
||||||
@ -201,10 +246,108 @@ async fn handle_command(
|
|||||||
.send_payment_notification(cli.account, recipient, receipt, note)
|
.send_payment_notification(cli.account, recipient, receipt, note)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
CliCommands::SendPinMessage {
|
||||||
|
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 {
|
CliCommands::SendReaction {
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
|
username,
|
||||||
note_to_self,
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
emoji,
|
emoji,
|
||||||
target_author,
|
target_author,
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
@ -216,7 +359,9 @@ async fn handle_command(
|
|||||||
cli.account,
|
cli.account,
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
|
username,
|
||||||
note_to_self,
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
emoji,
|
emoji,
|
||||||
target_author,
|
target_author,
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
@ -227,6 +372,7 @@ async fn handle_command(
|
|||||||
}
|
}
|
||||||
CliCommands::SendReceipt {
|
CliCommands::SendReceipt {
|
||||||
recipient,
|
recipient,
|
||||||
|
username,
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
r#type,
|
r#type,
|
||||||
} => {
|
} => {
|
||||||
@ -234,6 +380,7 @@ async fn handle_command(
|
|||||||
.send_receipt(
|
.send_receipt(
|
||||||
cli.account,
|
cli.account,
|
||||||
recipient,
|
recipient,
|
||||||
|
username,
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
match r#type {
|
match r#type {
|
||||||
cli::ReceiptType::Read => "read".to_owned(),
|
cli::ReceiptType::Read => "read".to_owned(),
|
||||||
@ -252,6 +399,30 @@ async fn handle_command(
|
|||||||
.send_typing(cli.account, recipient, group_id, stop)
|
.send_typing(cli.account, recipient, group_id, stop)
|
||||||
.await
|
.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::SetPin { pin } => client.set_pin(cli.account, pin).await,
|
||||||
CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
|
CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
|
||||||
client
|
client
|
||||||
@ -284,6 +455,8 @@ async fn handle_command(
|
|||||||
unrestricted_unidentified_sender,
|
unrestricted_unidentified_sender,
|
||||||
discoverable_by_number,
|
discoverable_by_number,
|
||||||
number_sharing,
|
number_sharing,
|
||||||
|
username,
|
||||||
|
delete_username,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.update_account(
|
.update_account(
|
||||||
@ -292,6 +465,8 @@ async fn handle_command(
|
|||||||
unrestricted_unidentified_sender,
|
unrestricted_unidentified_sender,
|
||||||
discoverable_by_number,
|
discoverable_by_number,
|
||||||
number_sharing,
|
number_sharing,
|
||||||
|
username,
|
||||||
|
delete_username,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -315,9 +490,32 @@ async fn handle_command(
|
|||||||
recipient,
|
recipient,
|
||||||
expiration,
|
expiration,
|
||||||
name,
|
name,
|
||||||
|
given_name,
|
||||||
|
family_name,
|
||||||
|
nick_given_name,
|
||||||
|
nick_family_name,
|
||||||
|
note,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.update_contact(cli.account, recipient, name, expiration)
|
.update_contact(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
name,
|
||||||
|
expiration,
|
||||||
|
given_name,
|
||||||
|
family_name,
|
||||||
|
nick_given_name,
|
||||||
|
nick_family_name,
|
||||||
|
note,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::UpdateDevice {
|
||||||
|
device_id,
|
||||||
|
device_name,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.update_device(cli.account, device_id, device_name)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
CliCommands::UpdateGroup {
|
CliCommands::UpdateGroup {
|
||||||
@ -337,6 +535,8 @@ async fn handle_command(
|
|||||||
set_permission_edit_details,
|
set_permission_edit_details,
|
||||||
set_permission_send_messages,
|
set_permission_send_messages,
|
||||||
expiration,
|
expiration,
|
||||||
|
member_label_emoji,
|
||||||
|
member_label,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.update_group(
|
.update_group(
|
||||||
@ -370,6 +570,8 @@ async fn handle_command(
|
|||||||
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
||||||
}),
|
}),
|
||||||
expiration,
|
expiration,
|
||||||
|
member_label_emoji,
|
||||||
|
member_label,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,42 @@
|
|||||||
<content_attribute id="social-chat">intense</content_attribute>
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
</content_rating>
|
</content_rating>
|
||||||
<releases>
|
<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">
|
<release version="0.13.20" date="2025-09-23">
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.20</url>
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.20</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
359
docs/CALL_TUNNEL.md
Normal file
359
docs/CALL_TUNNEL.md
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
@ -1,343 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name":"[B"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"[Ljava.lang.String;"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"[Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"[[B"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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":"getClassLoader","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.ClassLoader",
|
|
||||||
"methods":[{"name":"getPlatformClassLoader","parameterTypes":[] }, {"name":"loadClass","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.ClassNotFoundException"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.Enum",
|
|
||||||
"methods":[{"name":"ordinal","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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.Long",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.NoClassDefFoundError"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.NoSuchMethodError"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.String"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.Thread",
|
|
||||||
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.Throwable",
|
|
||||||
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.UnsatisfiedLinkError",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.util.HashMap",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.util.Map",
|
|
||||||
"methods":[{"name":"get","parameterTypes":["java.lang.Object"] }, {"name":"put","parameterTypes":["java.lang.Object","java.lang.Object"] }, {"name":"remove","parameterTypes":["java.lang.Object"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.util.UUID",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long","long"] }, {"name":"getLeastSignificantBits","parameterTypes":[] }, {"name":"getMostSignificantBits","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.util.concurrent.ForkJoinWorkerThread"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"jdk.internal.loader.ClassLoaders$AppClassLoader"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.asamk.signal.manager.internal.SignalWebSocketHealthMonitor$KeepAliveSender"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.asamk.signal.manager.storage.protocol.SignalProtocolStore",
|
|
||||||
"methods":[{"name":"getIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress"] }, {"name":"getIdentityKeyPair","parameterTypes":[] }, {"name":"getLocalRegistrationId","parameterTypes":[] }, {"name":"isTrustedIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.IdentityKey","org.signal.libsignal.protocol.state.IdentityKeyStore$Direction"] }, {"name":"loadKyberPreKey","parameterTypes":["int"] }, {"name":"loadPreKey","parameterTypes":["int"] }, {"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] }, {"name":"loadSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress"] }, {"name":"loadSignedPreKey","parameterTypes":["int"] }, {"name":"markKyberPreKeyUsed","parameterTypes":["int"] }, {"name":"markKyberPreKeyUsed","parameterTypes":["int","int","org.signal.libsignal.protocol.ecc.ECPublicKey"] }, {"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.internal.CompletableFuture",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }, {"name":"completeExceptionally","parameterTypes":["java.lang.Throwable"] }, {"name":"setCancellationId","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner",
|
|
||||||
"methods":[{"name":"unsafeNativeHandleWithoutGuard","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.CdsiLookupResponse",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.util.Map","int"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.CdsiLookupResponse$Entry",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["byte[]","byte[]"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.ChatConnection$ListenerBridge",
|
|
||||||
"methods":[{"name":"onConnectionInterrupted","parameterTypes":["java.lang.Throwable"] }, {"name":"onIncomingMessage","parameterTypes":["byte[]","long","long"] }, {"name":"onQueueEmpty","parameterTypes":[] }, {"name":"onReceivedAlerts","parameterTypes":["java.lang.String[]"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.ChatConnection$Response",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["int","java.lang.String","java.util.Map","byte[]"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.ChatService"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.ChatService$DebugInfo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.ChatService$Response"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.ChatService$ResponseAndDebugInfo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.DeviceDeregisteredException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.NetworkException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.RetryLaterException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.net.internal.BridgeChatListener"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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.ecc.ECPublicKey",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.fingerprint.FingerprintParsingException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.logging.Log",
|
|
||||||
"methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.message.PlaintextContent",
|
|
||||||
"fields":[{"name":"unsafeHandle"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.message.PreKeySignalMessage",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.message.SenderKeyMessage",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.message.SignalMessage",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
|
|
||||||
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.KyberPreKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.PreKeyRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.PreKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.SessionRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["byte[]"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.SessionStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.SignedPreKeyRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.SignedPreKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.usernames.BadDiscriminatorCharacterException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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.DiscriminatorCannotBeZeroException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.usernames.MissingSeparatorException",
|
|
||||||
"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"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.whispersystems.signalservice.api.websocket.SignalWebSocket$DelayedDisconnectThread"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"type":"agent-extracted",
|
|
||||||
"classes":[
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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.Signal$Identity"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"interfaces":["org.asamk.SignalControl"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"interfaces":["org.freedesktop.dbus.interfaces.DBus"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,226 +0,0 @@
|
|||||||
{
|
|
||||||
"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/com.sun.net.httpserver.spi.HttpServerProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.net.spi.InetAddressResolverProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.nio.file.spi.FileTypeDetector\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.sql.Driver\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.util.spi.ResourceBundleControlProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.util.ModuleVisibilityHelper\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.message.ISocketProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.transport.ITransportProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AG\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AI\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AS\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AT\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AU\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AZ\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BB\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BM\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BO\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BS\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CA\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CH\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CI\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CL\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CN\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CO\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DK\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EC\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ES\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FI\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GB\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HK\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HU\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ID\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IN\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IT\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_JP\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_LV\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MM\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MO\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MX\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MY\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NG\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NL\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PA\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PH\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PL\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RO\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RU\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SA\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SI\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SK\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TH\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UA\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UG\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_VE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_XK\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/annotation/annotation.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/collections/collections.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/coroutines/coroutines.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/internal/internal.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/jvm/jvm.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/kotlin.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/ranges/ranges.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/reflect/reflect.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qlibsignal_jni.so\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qlibsignal_jni_aarch64.dylib\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qlibsignal_jni_amd64.dylib\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qlibsignal_jni_amd64.so\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qorg/asamk/signal/manager/config/whisper.store\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qorg/sqlite/native/Linux/x86_64/libsqlitejdbc.so\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qsignal_jni.dll\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qsignal_jni_amd64.dll\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qsqlite-jdbc.properties\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"com/google/i18n/phonenumbers/data/.*"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/nfc.nrm\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/uprops.icu\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/nfc.nrm\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/uprops.icu\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qsun/net/idn/uidna.spp\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qsun/net/www/content-types.properties\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
|
|
||||||
}]},
|
|
||||||
"bundles":[{
|
|
||||||
"name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl",
|
|
||||||
"locales":["", "de", "en", "und"]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"types":[
|
|
||||||
],
|
|
||||||
"lambdaCapturingTypes":[
|
|
||||||
],
|
|
||||||
"proxies":[
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,17 +1,29 @@
|
|||||||
[versions]
|
[versions]
|
||||||
slf4j = "2.0.17"
|
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]
|
[libraries]
|
||||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.81"
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85"
|
||||||
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.20.0"
|
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.1"
|
||||||
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
|
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
|
||||||
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.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-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||||
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
||||||
logback = "ch.qos.logback:logback-classic:1.5.18"
|
logback = "ch.qos.logback:logback-classic:1.6.1"
|
||||||
|
|
||||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_130"
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
sqlite = "org.xerial:sqlite-jdbc:3.50.3.0"
|
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
|
||||||
hikari = "com.zaxxer:HikariCP:7.0.2"
|
sqlite = "org.xerial:sqlite-jdbc:3.53.2.1"
|
||||||
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.4"
|
hikari = "com.zaxxer:HikariCP:7.1.0"
|
||||||
junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.4"
|
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.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,7 +1,9 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
6
gradlew
vendored
6
gradlew
vendored
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
# gradlew start up script for POSIX generated by Gradle.
|
||||||
#
|
#
|
||||||
# Important for running:
|
# Important for running:
|
||||||
#
|
#
|
||||||
@ -29,7 +29,7 @@
|
|||||||
# bash, then to run this script, type that shell name before the whole
|
# bash, then to run this script, type that shell name before the whole
|
||||||
# command line, like:
|
# command line, like:
|
||||||
#
|
#
|
||||||
# ksh Gradle
|
# ksh gradlew
|
||||||
#
|
#
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
35
gradlew.bat
vendored
35
gradlew.bat
vendored
@ -19,12 +19,12 @@
|
|||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem gradlew startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@ -51,7 +51,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@ -65,29 +65,18 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute gradlew
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
@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
|
||||||
|
|
||||||
:end
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
: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,11 +4,13 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_25
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_25
|
||||||
|
|
||||||
toolchain {
|
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
||||||
languageVersion.set(JavaLanguageVersion.of(21))
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,9 +18,9 @@ val libsignalClientPath = project.findProperty("libsignal_client_path")?.toStrin
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
if (libsignalClientPath == null) {
|
if (libsignalClientPath == null) {
|
||||||
implementation(libs.signalservice)
|
implementation(libs.signalnetwork)
|
||||||
} else {
|
} else {
|
||||||
implementation(libs.signalservice) {
|
implementation(libs.signalnetwork) {
|
||||||
exclude(group = "org.signal", module = "libsignal-client")
|
exclude(group = "org.signal", module = "libsignal-client")
|
||||||
}
|
}
|
||||||
implementation(files(libsignalClientPath))
|
implementation(files(libsignalClientPath))
|
||||||
@ -28,8 +30,10 @@ dependencies {
|
|||||||
implementation(libs.slf4j.api)
|
implementation(libs.slf4j.api)
|
||||||
implementation(libs.sqlite)
|
implementation(libs.sqlite)
|
||||||
implementation(libs.hikari)
|
implementation(libs.hikari)
|
||||||
|
compileOnly(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
testImplementation(libs.junit.jupiter)
|
testImplementation(libs.junit.jupiter)
|
||||||
|
testImplementation(platform(libs.junit.jupiter.bom))
|
||||||
testRuntimeOnly(libs.junit.launcher)
|
testRuntimeOnly(libs.junit.launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
|||||||
|
|
||||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||||
|
import org.asamk.signal.manager.api.CallInfo;
|
||||||
|
import org.asamk.signal.manager.api.CallOffer;
|
||||||
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
||||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.Configuration;
|
import org.asamk.signal.manager.api.Configuration;
|
||||||
@ -37,11 +39,13 @@ import org.asamk.signal.manager.api.ReceiveConfig;
|
|||||||
import org.asamk.signal.manager.api.Recipient;
|
import org.asamk.signal.manager.api.Recipient;
|
||||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||||
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
||||||
|
import org.asamk.signal.manager.api.SendMessageResult;
|
||||||
import org.asamk.signal.manager.api.SendMessageResults;
|
import org.asamk.signal.manager.api.SendMessageResults;
|
||||||
import org.asamk.signal.manager.api.StickerPack;
|
import org.asamk.signal.manager.api.StickerPack;
|
||||||
import org.asamk.signal.manager.api.StickerPackId;
|
import org.asamk.signal.manager.api.StickerPackId;
|
||||||
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
||||||
import org.asamk.signal.manager.api.StickerPackUrl;
|
import org.asamk.signal.manager.api.StickerPackUrl;
|
||||||
|
import org.asamk.signal.manager.api.TurnServer;
|
||||||
import org.asamk.signal.manager.api.TypingAction;
|
import org.asamk.signal.manager.api.TypingAction;
|
||||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||||
import org.asamk.signal.manager.api.UpdateGroup;
|
import org.asamk.signal.manager.api.UpdateGroup;
|
||||||
@ -50,6 +54,7 @@ import org.asamk.signal.manager.api.UserStatus;
|
|||||||
import org.asamk.signal.manager.api.UsernameLinkUrl;
|
import org.asamk.signal.manager.api.UsernameLinkUrl;
|
||||||
import org.asamk.signal.manager.api.UsernameStatus;
|
import org.asamk.signal.manager.api.UsernameStatus;
|
||||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -70,6 +75,10 @@ public interface Manager extends Closeable {
|
|||||||
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean isValidAci(final String aci) {
|
||||||
|
return UuidUtil.INSTANCE.isUuid(aci);
|
||||||
|
}
|
||||||
|
|
||||||
static boolean isSignalClientAvailable() {
|
static boolean isSignalClientAvailable() {
|
||||||
final Logger logger = LoggerFactory.getLogger(Manager.class);
|
final Logger logger = LoggerFactory.getLogger(Manager.class);
|
||||||
try {
|
try {
|
||||||
@ -87,6 +96,8 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
String getSelfNumber();
|
String getSelfNumber();
|
||||||
|
|
||||||
|
String getSelfACI();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is used for checking a set of phone numbers for registration on Signal
|
* This is used for checking a set of phone numbers for registration on Signal
|
||||||
*
|
*
|
||||||
@ -154,6 +165,8 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
List<Device> getLinkedDevices() throws IOException;
|
List<Device> getLinkedDevices() throws IOException;
|
||||||
|
|
||||||
|
void updateLinkedDevice(int deviceId, String name) throws IOException, NotPrimaryDeviceException;
|
||||||
|
|
||||||
void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException;
|
void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException;
|
||||||
|
|
||||||
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException;
|
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException;
|
||||||
@ -171,6 +184,10 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
void deleteGroup(GroupId groupId) throws IOException;
|
void deleteGroup(GroupId groupId) throws IOException;
|
||||||
|
|
||||||
|
SendGroupMessageResults terminateGroup(
|
||||||
|
GroupId groupId
|
||||||
|
) throws IOException, GroupNotFoundException, NotAGroupMemberException;
|
||||||
|
|
||||||
Pair<GroupId, SendGroupMessageResults> createGroup(
|
Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
String name,
|
String name,
|
||||||
Set<RecipientIdentifier.Single> members,
|
Set<RecipientIdentifier.Single> members,
|
||||||
@ -207,6 +224,19 @@ public interface Manager extends Closeable {
|
|||||||
long editTargetTimestamp
|
long editTargetTimestamp
|
||||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post a file attachment story to "My Story" or to a group.
|
||||||
|
*
|
||||||
|
* @param attachment path to the file to upload and post as a story
|
||||||
|
* @param allowsReplies whether other users are allowed to reply to this story
|
||||||
|
* @param groupId if present, post the story to this group instead of "My Story"
|
||||||
|
*/
|
||||||
|
SendMessageResults sendStory(
|
||||||
|
String attachment,
|
||||||
|
boolean allowsReplies,
|
||||||
|
Optional<GroupId> groupId
|
||||||
|
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException;
|
||||||
|
|
||||||
SendMessageResults sendRemoteDeleteMessage(
|
SendMessageResults sendRemoteDeleteMessage(
|
||||||
long targetSentTimestamp,
|
long targetSentTimestamp,
|
||||||
Set<RecipientIdentifier> recipients
|
Set<RecipientIdentifier> recipients
|
||||||
@ -218,22 +248,71 @@ public interface Manager extends Closeable {
|
|||||||
RecipientIdentifier.Single targetAuthor,
|
RecipientIdentifier.Single targetAuthor,
|
||||||
long targetSentTimestamp,
|
long targetSentTimestamp,
|
||||||
Set<RecipientIdentifier> recipients,
|
Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf,
|
||||||
final boolean isStory
|
final boolean isStory
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendAdminDelete(
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier.Group> recipients,
|
||||||
|
boolean notifySelf,
|
||||||
|
boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendPinMessage(
|
||||||
|
int pinDuration,
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier> recipients,
|
||||||
|
boolean notifySelf,
|
||||||
|
boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendUnpinMessage(
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier> recipients,
|
||||||
|
boolean notifySelf,
|
||||||
|
boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
SendMessageResults sendPaymentNotificationMessage(
|
SendMessageResults sendPaymentNotificationMessage(
|
||||||
byte[] receipt,
|
byte[] receipt,
|
||||||
String note,
|
String note,
|
||||||
RecipientIdentifier.Single recipient
|
RecipientIdentifier.Single recipient
|
||||||
) throws IOException;
|
) throws IOException;
|
||||||
|
|
||||||
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
||||||
|
|
||||||
SendMessageResults sendMessageRequestResponse(
|
SendMessageResults sendMessageRequestResponse(
|
||||||
MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
||||||
Set<RecipientIdentifier> recipientIdentifiers
|
Set<RecipientIdentifier> recipientIdentifiers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
SendMessageResults sendPollCreateMessage(
|
||||||
|
final String question,
|
||||||
|
final boolean allowMultiple,
|
||||||
|
final List<String> options,
|
||||||
|
final Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendPollVoteMessage(
|
||||||
|
final RecipientIdentifier.Single targetAuthor,
|
||||||
|
final long targetSentTimestamp,
|
||||||
|
final List<Integer> optionIndexes,
|
||||||
|
final int voteCount,
|
||||||
|
final Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendPollTerminateMessage(
|
||||||
|
final long targetSentTimestamp,
|
||||||
|
final Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
void hideRecipient(RecipientIdentifier.Single recipient);
|
void hideRecipient(RecipientIdentifier.Single recipient);
|
||||||
|
|
||||||
void deleteRecipient(RecipientIdentifier.Single recipient);
|
void deleteRecipient(RecipientIdentifier.Single recipient);
|
||||||
@ -247,7 +326,7 @@ public interface Manager extends Closeable {
|
|||||||
final String nickGivenName,
|
final String nickGivenName,
|
||||||
final String nickFamilyName,
|
final String nickFamilyName,
|
||||||
final String note
|
final String note
|
||||||
) throws NotPrimaryDeviceException, UnregisteredRecipientException;
|
) throws UnregisteredRecipientException;
|
||||||
|
|
||||||
void setContactsBlocked(
|
void setContactsBlocked(
|
||||||
Collection<RecipientIdentifier.Single> recipient,
|
Collection<RecipientIdentifier.Single> recipient,
|
||||||
@ -362,9 +441,52 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException;
|
InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException;
|
||||||
|
|
||||||
|
// --- Voice call methods ---
|
||||||
|
|
||||||
|
CallInfo startCall(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
CallInfo acceptCall(long callId) throws IOException;
|
||||||
|
|
||||||
|
void hangupCall(long callId) throws IOException;
|
||||||
|
|
||||||
|
SendMessageResult rejectCall(long callId) throws IOException;
|
||||||
|
|
||||||
|
List<CallInfo> listActiveCalls();
|
||||||
|
|
||||||
|
void sendCallOffer(
|
||||||
|
RecipientIdentifier.Single recipient,
|
||||||
|
CallOffer offer
|
||||||
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void sendCallAnswer(
|
||||||
|
RecipientIdentifier.Single recipient,
|
||||||
|
long callId,
|
||||||
|
byte[] answerOpaque
|
||||||
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void sendIceUpdate(
|
||||||
|
RecipientIdentifier.Single recipient,
|
||||||
|
long callId,
|
||||||
|
List<byte[]> iceCandidates
|
||||||
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void sendHangup(
|
||||||
|
RecipientIdentifier.Single recipient,
|
||||||
|
long callId,
|
||||||
|
MessageEnvelope.Call.Hangup.Type type
|
||||||
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void sendBusy(RecipientIdentifier.Single recipient, long callId) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
List<TurnServer> getTurnServerInfo() throws IOException;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
|
void addCallEventListener(CallEventListener listener);
|
||||||
|
|
||||||
|
void removeCallEventListener(CallEventListener listener);
|
||||||
|
|
||||||
interface ReceiveMessageHandler {
|
interface ReceiveMessageHandler {
|
||||||
|
|
||||||
ReceiveMessageHandler EMPTY = (envelope, e) -> {
|
ReceiveMessageHandler EMPTY = (envelope, e) -> {
|
||||||
@ -372,4 +494,9 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
void handleMessage(MessageEnvelope envelope, Throwable e);
|
void handleMessage(MessageEnvelope envelope, Throwable e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CallEventListener {
|
||||||
|
|
||||||
|
void handleCallEvent(CallInfo callInfo, String reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
package org.asamk.signal.manager;
|
package org.asamk.signal.manager;
|
||||||
|
|
||||||
import org.asamk.signal.manager.internal.LibSignalLogger;
|
import org.asamk.signal.manager.internal.LibSignalLogger;
|
||||||
|
import org.asamk.signal.manager.internal.SignalLogger;
|
||||||
|
|
||||||
public class ManagerLogger {
|
public class ManagerLogger {
|
||||||
|
|
||||||
public static void initLogger() {
|
public static void initLogger() {
|
||||||
LibSignalLogger.initLogger();
|
LibSignalLogger.initLogger();
|
||||||
|
SignalLogger.initLogger();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import org.asamk.signal.manager.internal.RegistrationManagerImpl;
|
|||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
@ -64,10 +65,10 @@ public class SignalAccountFiles {
|
|||||||
return accountsStore.getAllNumbers();
|
return accountsStore.getAllNumbers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException {
|
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||||
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||||
try {
|
try {
|
||||||
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
|
return new Pair<Manager, Throwable>(initManagerByNumber(a.number(), a.path()), null);
|
||||||
} catch (NotRegisteredException e) {
|
} catch (NotRegisteredException e) {
|
||||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||||
return null;
|
return null;
|
||||||
@ -80,24 +81,41 @@ public class SignalAccountFiles {
|
|||||||
for (final var pair : managerPairs) {
|
for (final var pair : managerPairs) {
|
||||||
if (pair.second() instanceof IOException e) {
|
if (pair.second() instanceof IOException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} else if (pair.second() instanceof AccountCheckException e) {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var managers = managerPairs.stream().map(Pair::first).toList();
|
final var managers = managerPairs.stream()
|
||||||
|
.filter(p -> p != null && p.first() != null)
|
||||||
|
.map(Pair::first)
|
||||||
|
.toList();
|
||||||
return new MultiAccountManagerImpl(managers, this);
|
return new MultiAccountManagerImpl(managers, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
final var accountPath = accountsStore.getPathByNumber(number);
|
final var accountPath = accountsStore.getPathByNumber(number);
|
||||||
return this.initManager(number, accountPath);
|
return this.initManagerByNumber(number, accountPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Manager initManager(
|
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 number,
|
||||||
String accountPath
|
String accountPath
|
||||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
|
final var account = loadAccount(accountPath);
|
||||||
|
if (!number.equals(account.getNumber())) {
|
||||||
|
account.close();
|
||||||
|
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
return initManagerFromAccount(number, accountPath, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalAccount loadAccount(final String accountPath) throws NotRegisteredException, IOException {
|
||||||
if (accountPath == null) {
|
if (accountPath == null) {
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
}
|
}
|
||||||
@ -105,12 +123,27 @@ public class SignalAccountFiles {
|
|||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||||
if (!number.equals(account.getNumber())) {
|
}
|
||||||
|
|
||||||
|
private Manager initManagerByAci(
|
||||||
|
ACI aci,
|
||||||
|
String accountPath
|
||||||
|
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
|
final var account = loadAccount(accountPath);
|
||||||
|
if (!aci.equals(account.getAci())) {
|
||||||
account.close();
|
account.close();
|
||||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return initManagerFromAccount(aci.toString(), accountPath, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ManagerImpl initManagerFromAccount(
|
||||||
|
final String identifier,
|
||||||
|
final String accountPath,
|
||||||
|
final SignalAccount account
|
||||||
|
) throws NotRegisteredException, IOException, AccountCheckException {
|
||||||
if (!account.isRegistered()) {
|
if (!account.isRegistered()) {
|
||||||
account.close();
|
account.close();
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
@ -132,10 +165,10 @@ public class SignalAccountFiles {
|
|||||||
manager.checkAccountState();
|
manager.checkAccountState();
|
||||||
} catch (DeprecatedVersionException e) {
|
} catch (DeprecatedVersionException e) {
|
||||||
manager.close();
|
manager.close();
|
||||||
throw new AccountCheckException("signal-cli version is too old for the Signal-Server, please update.");
|
throw new IOException("signal-cli version is too old for the Signal-Server, please update.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
manager.close();
|
manager.close();
|
||||||
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
|
throw new AccountCheckException("Error while checking account " + identifier + ": " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.getServiceEnvironment() == null) {
|
if (account.getServiceEnvironment() == null) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package org.asamk.signal.manager.actions;
|
|||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
import org.asamk.signal.manager.helper.Context;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.signal.core.models.ServiceId;
|
||||||
|
|
||||||
public class RenewSessionAction implements HandleAction {
|
public class RenewSessionAction implements HandleAction {
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,7 +60,7 @@ public class SendRetryMessageRequestAction implements HandleAction {
|
|||||||
return CiphertextMessage.WHISPER_TYPE;
|
return CiphertextMessage.WHISPER_TYPE;
|
||||||
}
|
}
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
case PREKEY_BUNDLE -> CiphertextMessage.PREKEY_TYPE;
|
case PREKEY_MESSAGE -> CiphertextMessage.PREKEY_TYPE;
|
||||||
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
|
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
|
||||||
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
|
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
|
||||||
default -> CiphertextMessage.WHISPER_TYPE;
|
default -> CiphertextMessage.WHISPER_TYPE;
|
||||||
|
|||||||
21
lib/src/main/java/org/asamk/signal/manager/api/CallInfo.java
Normal file
21
lib/src/main/java/org/asamk/signal/manager/api/CallInfo.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
public record CallOffer(
|
||||||
|
long callId, Type type, byte[] opaque
|
||||||
|
) {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
AUDIO,
|
||||||
|
VIDEO
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,11 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
public class CaptchaRequiredException extends Exception {
|
public class CaptchaRequiredException extends Exception {
|
||||||
|
|
||||||
private long nextAttemptTimestamp;
|
private long nextVerificationAttemptMilliseconds;
|
||||||
|
|
||||||
public CaptchaRequiredException(final long nextAttemptTimestamp) {
|
public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) {
|
||||||
super("Captcha required");
|
super("Captcha required");
|
||||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CaptchaRequiredException(final String message) {
|
public CaptchaRequiredException(final String message) {
|
||||||
@ -17,7 +17,7 @@ public class CaptchaRequiredException extends Exception {
|
|||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getNextAttemptTimestamp() {
|
public long getNextVerificationAttemptMilliseconds() {
|
||||||
return nextAttemptTimestamp;
|
return nextVerificationAttemptMilliseconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ public record Contact(
|
|||||||
long muteUntil,
|
long muteUntil,
|
||||||
boolean hideStory,
|
boolean hideStory,
|
||||||
boolean isBlocked,
|
boolean isBlocked,
|
||||||
|
long blockedAt,
|
||||||
boolean isArchived,
|
boolean isArchived,
|
||||||
boolean isProfileSharingEnabled,
|
boolean isProfileSharingEnabled,
|
||||||
boolean isHidden,
|
boolean isHidden,
|
||||||
@ -34,6 +35,7 @@ public record Contact(
|
|||||||
builder.muteUntil,
|
builder.muteUntil,
|
||||||
builder.hideStory,
|
builder.hideStory,
|
||||||
builder.isBlocked,
|
builder.isBlocked,
|
||||||
|
builder.blockedAt,
|
||||||
builder.isArchived,
|
builder.isArchived,
|
||||||
builder.isProfileSharingEnabled,
|
builder.isProfileSharingEnabled,
|
||||||
builder.isHidden,
|
builder.isHidden,
|
||||||
@ -58,6 +60,7 @@ public record Contact(
|
|||||||
builder.muteUntil = copy.muteUntil();
|
builder.muteUntil = copy.muteUntil();
|
||||||
builder.hideStory = copy.hideStory();
|
builder.hideStory = copy.hideStory();
|
||||||
builder.isBlocked = copy.isBlocked();
|
builder.isBlocked = copy.isBlocked();
|
||||||
|
builder.blockedAt = copy.blockedAt();
|
||||||
builder.isArchived = copy.isArchived();
|
builder.isArchived = copy.isArchived();
|
||||||
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
||||||
builder.isHidden = copy.isHidden();
|
builder.isHidden = copy.isHidden();
|
||||||
@ -80,6 +83,21 @@ public record Contact(
|
|||||||
return givenName + " " + familyName;
|
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 {
|
public static final class Builder {
|
||||||
|
|
||||||
private String givenName;
|
private String givenName;
|
||||||
@ -94,6 +112,7 @@ public record Contact(
|
|||||||
private long muteUntil;
|
private long muteUntil;
|
||||||
private boolean hideStory;
|
private boolean hideStory;
|
||||||
private boolean isBlocked;
|
private boolean isBlocked;
|
||||||
|
private long blockedAt;
|
||||||
private boolean isArchived;
|
private boolean isArchived;
|
||||||
private boolean isProfileSharingEnabled;
|
private boolean isProfileSharingEnabled;
|
||||||
private boolean isHidden;
|
private boolean isHidden;
|
||||||
@ -162,10 +181,20 @@ public record Contact(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Builder withIsBlocked(final boolean val) {
|
public Builder withIsBlocked(final boolean val) {
|
||||||
|
if (val && !isBlocked) {
|
||||||
|
blockedAt = System.currentTimeMillis();
|
||||||
|
} else if (!val) {
|
||||||
|
blockedAt = 0;
|
||||||
|
}
|
||||||
isBlocked = val;
|
isBlocked = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withBlockedAt(final long val) {
|
||||||
|
blockedAt = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder withIsArchived(final boolean val) {
|
public Builder withIsArchived(final boolean val) {
|
||||||
isArchived = val;
|
isArchived = val;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@ -12,10 +12,9 @@ public record Group(
|
|||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
GroupInviteLinkUrl groupInviteLinkUrl,
|
GroupInviteLinkUrl groupInviteLinkUrl,
|
||||||
Set<RecipientAddress> members,
|
Set<GroupMember> members,
|
||||||
Set<RecipientAddress> pendingMembers,
|
Set<RecipientAddress> pendingMembers,
|
||||||
Set<RecipientAddress> requestingMembers,
|
Set<RecipientAddress> requestingMembers,
|
||||||
Set<RecipientAddress> adminMembers,
|
|
||||||
Set<RecipientAddress> bannedMembers,
|
Set<RecipientAddress> bannedMembers,
|
||||||
boolean isBlocked,
|
boolean isBlocked,
|
||||||
int messageExpirationTimer,
|
int messageExpirationTimer,
|
||||||
@ -23,7 +22,8 @@ public record Group(
|
|||||||
GroupPermission permissionEditDetails,
|
GroupPermission permissionEditDetails,
|
||||||
GroupPermission permissionSendMessage,
|
GroupPermission permissionSendMessage,
|
||||||
boolean isMember,
|
boolean isMember,
|
||||||
boolean isAdmin
|
boolean isAdmin,
|
||||||
|
boolean isTerminated
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static Group from(
|
public static Group from(
|
||||||
@ -37,8 +37,7 @@ public record Group(
|
|||||||
groupInfo.getGroupInviteLink(),
|
groupInfo.getGroupInviteLink(),
|
||||||
groupInfo.getMembers()
|
groupInfo.getMembers()
|
||||||
.stream()
|
.stream()
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
.map(m -> org.asamk.signal.manager.api.GroupMember.from(m, recipientStore))
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
|
||||||
.collect(Collectors.toSet()),
|
.collect(Collectors.toSet()),
|
||||||
groupInfo.getPendingMembers()
|
groupInfo.getPendingMembers()
|
||||||
.stream()
|
.stream()
|
||||||
@ -50,11 +49,6 @@ public record Group(
|
|||||||
.map(recipientStore::resolveRecipientAddress)
|
.map(recipientStore::resolveRecipientAddress)
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
||||||
.collect(Collectors.toSet()),
|
.collect(Collectors.toSet()),
|
||||||
groupInfo.getAdminMembers()
|
|
||||||
.stream()
|
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
|
||||||
.collect(Collectors.toSet()),
|
|
||||||
groupInfo.getBannedMembers()
|
groupInfo.getBannedMembers()
|
||||||
.stream()
|
.stream()
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
.map(recipientStore::resolveRecipientAddress)
|
||||||
@ -66,6 +60,7 @@ public record Group(
|
|||||||
groupInfo.getPermissionEditDetails(),
|
groupInfo.getPermissionEditDetails(),
|
||||||
groupInfo.getPermissionSendMessage(),
|
groupInfo.getPermissionSendMessage(),
|
||||||
groupInfo.isMember(selfRecipientId),
|
groupInfo.isMember(selfRecipientId),
|
||||||
groupInfo.isAdmin(selfRecipientId));
|
groupInfo.isAdmin(selfRecipientId),
|
||||||
|
groupInfo.isTerminated());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import org.asamk.signal.manager.groups.GroupLinkPassword;
|
|||||||
import org.signal.core.util.Base64;
|
import org.signal.core.util.Base64;
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
import org.signal.storageservice.storage.protos.groups.GroupInviteLink;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@ -52,8 +52,8 @@ public final class GroupInviteLinkUrl {
|
|||||||
var bytes = Base64.decode(encoding);
|
var bytes = Base64.decode(encoding);
|
||||||
GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes);
|
GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes);
|
||||||
|
|
||||||
if (groupInviteLink.v1Contents != null) {
|
if (groupInviteLink.contentsV1 != null) {
|
||||||
var groupInviteLinkContentsV1 = groupInviteLink.v1Contents;
|
var groupInviteLinkContentsV1 = groupInviteLink.contentsV1;
|
||||||
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray());
|
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray());
|
||||||
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray());
|
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray());
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ public final class GroupInviteLinkUrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
||||||
var groupInviteLink = new GroupInviteLink.Builder().v1Contents(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey(
|
var groupInviteLink = new GroupInviteLink.Builder().contentsV1(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey(
|
||||||
ByteString.of(groupMasterKey.serialize()))
|
ByteString.of(groupMasterKey.serialize()))
|
||||||
.inviteLinkPassword(ByteString.of(password.serialize()))
|
.inviteLinkPassword(ByteString.of(password.serialize()))
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,12 +7,14 @@ public record Message(
|
|||||||
String messageText,
|
String messageText,
|
||||||
List<String> attachments,
|
List<String> attachments,
|
||||||
boolean viewOnce,
|
boolean viewOnce,
|
||||||
|
boolean voiceNote,
|
||||||
List<Mention> mentions,
|
List<Mention> mentions,
|
||||||
Optional<Quote> quote,
|
Optional<Quote> quote,
|
||||||
Optional<Sticker> sticker,
|
Optional<Sticker> sticker,
|
||||||
List<Preview> previews,
|
List<Preview> previews,
|
||||||
Optional<StoryReply> storyReply,
|
Optional<StoryReply> storyReply,
|
||||||
List<TextStyle> textStyles
|
List<TextStyle> textStyles,
|
||||||
|
boolean urgent
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package org.asamk.signal.manager.api;
|
|||||||
import org.asamk.signal.manager.groups.GroupUtils;
|
import org.asamk.signal.manager.groups.GroupUtils;
|
||||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||||
|
import org.asamk.signal.manager.util.MimeUtils;
|
||||||
import org.signal.libsignal.metadata.ProtocolException;
|
import org.signal.libsignal.metadata.ProtocolException;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
@ -32,11 +33,11 @@ import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptM
|
|||||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -115,17 +116,35 @@ public record MessageEnvelope(
|
|||||||
Optional<Long> remoteDeleteId,
|
Optional<Long> remoteDeleteId,
|
||||||
Optional<Sticker> sticker,
|
Optional<Sticker> sticker,
|
||||||
List<SharedContact> sharedContacts,
|
List<SharedContact> sharedContacts,
|
||||||
|
Optional<PollCreate> pollCreate,
|
||||||
|
Optional<PollVote> pollVote,
|
||||||
|
Optional<PollTerminate> pollTerminate,
|
||||||
List<Mention> mentions,
|
List<Mention> mentions,
|
||||||
List<Preview> previews,
|
List<Preview> previews,
|
||||||
List<TextStyle> textStyles
|
List<TextStyle> textStyles,
|
||||||
|
Optional<PinMessage> pinMessage,
|
||||||
|
Optional<UnpinMessage> unpinMessage,
|
||||||
|
Optional<AdminDelete> adminDelete
|
||||||
) {
|
) {
|
||||||
|
|
||||||
static Data from(
|
static Data from(
|
||||||
final SignalServiceDataMessage dataMessage,
|
final SignalServiceDataMessage dataMessage,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider
|
final AttachmentFileProvider fileProvider
|
||||||
) {
|
) {
|
||||||
|
var body = dataMessage.getBody();
|
||||||
|
if (dataMessage.getAttachments().isPresent()) {
|
||||||
|
for (final var attachment : dataMessage.getAttachments().get()) {
|
||||||
|
if (MimeUtils.LONG_TEXT.equals(attachment.getContentType()) && attachment.isPointer()) {
|
||||||
|
final var longBody = longTexts.get(attachment.asPointer().getRemoteId().toString());
|
||||||
|
if (longBody != null) {
|
||||||
|
body = Optional.of(longBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return new Data(dataMessage.getTimestamp(),
|
return new Data(dataMessage.getTimestamp(),
|
||||||
dataMessage.getGroupContext().map(GroupContext::from),
|
dataMessage.getGroupContext().map(GroupContext::from),
|
||||||
dataMessage.getStoryContext()
|
dataMessage.getStoryContext()
|
||||||
@ -133,11 +152,11 @@ public record MessageEnvelope(
|
|||||||
recipientResolver,
|
recipientResolver,
|
||||||
addressResolver)),
|
addressResolver)),
|
||||||
dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
|
dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
|
||||||
dataMessage.getBody(),
|
body,
|
||||||
dataMessage.getExpiresInSeconds(),
|
dataMessage.getExpiresInSeconds(),
|
||||||
dataMessage.isExpirationUpdate(),
|
dataMessage.isExpirationUpdate(),
|
||||||
dataMessage.isViewOnce(),
|
dataMessage.isViewOnce(),
|
||||||
dataMessage.isEndSession(),
|
false,
|
||||||
dataMessage.isProfileKeyUpdate(),
|
dataMessage.isProfileKeyUpdate(),
|
||||||
dataMessage.getProfileKey().isPresent(),
|
dataMessage.getProfileKey().isPresent(),
|
||||||
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
|
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
|
||||||
@ -155,6 +174,9 @@ public record MessageEnvelope(
|
|||||||
.map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
|
.map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
|
||||||
.toList())
|
.toList())
|
||||||
.orElse(List.of()),
|
.orElse(List.of()),
|
||||||
|
dataMessage.getPollCreate().map(PollCreate::from),
|
||||||
|
dataMessage.getPollVote().map(p -> PollVote.from(p, recipientResolver, addressResolver)),
|
||||||
|
dataMessage.getPollTerminate().map(PollTerminate::from),
|
||||||
dataMessage.getMentions()
|
dataMessage.getMentions()
|
||||||
.map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
|
.map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
|
||||||
.orElse(List.of()),
|
.orElse(List.of()),
|
||||||
@ -163,7 +185,10 @@ public record MessageEnvelope(
|
|||||||
.orElse(List.of()),
|
.orElse(List.of()),
|
||||||
dataMessage.getBodyRanges()
|
dataMessage.getBodyRanges()
|
||||||
.map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList())
|
.map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList())
|
||||||
.orElse(List.of()));
|
.orElse(List.of()),
|
||||||
|
dataMessage.getPinnedMessage().map(p -> PinMessage.from(p, recipientResolver, addressResolver)),
|
||||||
|
dataMessage.getUnpinnedMessage().map(p -> UnpinMessage.from(p, recipientResolver, addressResolver)),
|
||||||
|
dataMessage.getAdminDelete().map(p -> AdminDelete.from(p, recipientResolver, addressResolver)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
|
public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
|
||||||
@ -242,19 +267,19 @@ public record MessageEnvelope(
|
|||||||
quote.getMentions() == null
|
quote.getMentions() == null
|
||||||
? List.of()
|
? List.of()
|
||||||
: quote.getMentions()
|
: quote.getMentions()
|
||||||
.stream()
|
.stream()
|
||||||
.map(m -> Mention.from(m, recipientResolver, addressResolver))
|
.map(m -> Mention.from(m, recipientResolver, addressResolver))
|
||||||
.toList(),
|
.toList(),
|
||||||
quote.getAttachments() == null
|
quote.getAttachments() == null
|
||||||
? List.of()
|
? List.of()
|
||||||
: quote.getAttachments().stream().map(a -> Attachment.from(a, fileProvider)).toList(),
|
: quote.getAttachments().stream().map(a -> Attachment.from(a, fileProvider)).toList(),
|
||||||
quote.getBodyRanges() == null
|
quote.getBodyRanges() == null
|
||||||
? List.of()
|
? List.of()
|
||||||
: quote.getBodyRanges()
|
: quote.getBodyRanges()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(r -> r.style != null)
|
.filter(r -> r.style != null)
|
||||||
.map(TextStyle::from)
|
.map(TextStyle::from)
|
||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,6 +534,44 @@ public record MessageEnvelope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record PollCreate(
|
||||||
|
String question, boolean allowMultiple, List<String> options
|
||||||
|
) {
|
||||||
|
|
||||||
|
static PollCreate from(
|
||||||
|
SignalServiceDataMessage.PollCreate pollCreate
|
||||||
|
) {
|
||||||
|
return new PollCreate(pollCreate.getQuestion(), pollCreate.getAllowMultiple(), pollCreate.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PollVote(
|
||||||
|
RecipientAddress targetAuthor, long targetSentTimestamp, List<Integer> optionIndexes, int voteCount
|
||||||
|
) {
|
||||||
|
|
||||||
|
static PollVote from(
|
||||||
|
SignalServiceDataMessage.PollVote pollVote,
|
||||||
|
RecipientResolver recipientResolver,
|
||||||
|
RecipientAddressResolver addressResolver
|
||||||
|
) {
|
||||||
|
return new PollVote(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(pollVote.getTargetAuthor()))
|
||||||
|
.toApiRecipientAddress(),
|
||||||
|
pollVote.getTargetSentTimestamp(),
|
||||||
|
pollVote.getOptionIndexes(),
|
||||||
|
pollVote.getVoteCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PollTerminate(long targetSentTimestamp) {
|
||||||
|
|
||||||
|
static PollTerminate from(SignalServiceDataMessage.PollTerminate pollTerminate) {
|
||||||
|
return new PollTerminate(pollTerminate.getTargetSentTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
|
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
|
||||||
|
|
||||||
static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) {
|
static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) {
|
||||||
@ -520,18 +583,68 @@ public record MessageEnvelope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record PinMessage(
|
||||||
|
RecipientAddress targetAuthor, long targetSentTimestamp, long pinDurationSeconds
|
||||||
|
) {
|
||||||
|
|
||||||
|
static PinMessage from(
|
||||||
|
SignalServiceDataMessage.PinnedMessage pinnedMessage,
|
||||||
|
RecipientResolver recipientResolver,
|
||||||
|
RecipientAddressResolver addressResolver
|
||||||
|
) {
|
||||||
|
return new PinMessage(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
|
pinnedMessage.getTargetAuthor())).toApiRecipientAddress(),
|
||||||
|
pinnedMessage.getTargetSentTimestamp(),
|
||||||
|
Boolean.TRUE.equals(pinnedMessage.getForever())
|
||||||
|
? -1
|
||||||
|
: pinnedMessage.getPinDurationInSeconds() == null
|
||||||
|
? 0
|
||||||
|
: pinnedMessage.getPinDurationInSeconds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UnpinMessage(RecipientAddress targetAuthor, long targetSentTimestamp) {
|
||||||
|
|
||||||
|
static UnpinMessage from(
|
||||||
|
SignalServiceDataMessage.UnpinnedMessage unpinnedMessage,
|
||||||
|
RecipientResolver recipientResolver,
|
||||||
|
RecipientAddressResolver addressResolver
|
||||||
|
) {
|
||||||
|
return new UnpinMessage(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
|
unpinnedMessage.getTargetAuthor())).toApiRecipientAddress(),
|
||||||
|
unpinnedMessage.getTargetSentTimestamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AdminDelete(RecipientAddress targetAuthor, long targetSentTimestamp) {
|
||||||
|
|
||||||
|
static AdminDelete from(
|
||||||
|
SignalServiceDataMessage.AdminDelete adminDelete,
|
||||||
|
RecipientResolver recipientResolver,
|
||||||
|
RecipientAddressResolver addressResolver
|
||||||
|
) {
|
||||||
|
return new AdminDelete(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
|
adminDelete.getTargetAuthor())).toApiRecipientAddress(), adminDelete.getTargetSentTimestamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Edit(long targetSentTimestamp, Data dataMessage) {
|
public record Edit(long targetSentTimestamp, Data dataMessage) {
|
||||||
|
|
||||||
public static Edit from(
|
public static Edit from(
|
||||||
final SignalServiceEditMessage editMessage,
|
final SignalServiceEditMessage editMessage,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider
|
final AttachmentFileProvider fileProvider
|
||||||
) {
|
) {
|
||||||
return new Edit(editMessage.getTargetSentTimestamp(),
|
return new Edit(editMessage.getTargetSentTimestamp(),
|
||||||
Data.from(editMessage.getDataMessage(), recipientResolver, addressResolver, fileProvider));
|
Data.from(editMessage.getDataMessage(),
|
||||||
|
longTexts,
|
||||||
|
recipientResolver,
|
||||||
|
addressResolver,
|
||||||
|
fileProvider));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,12 +661,13 @@ public record MessageEnvelope(
|
|||||||
|
|
||||||
public static Sync from(
|
public static Sync from(
|
||||||
final SignalServiceSyncMessage syncMessage,
|
final SignalServiceSyncMessage syncMessage,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider
|
final AttachmentFileProvider fileProvider
|
||||||
) {
|
) {
|
||||||
return new Sync(syncMessage.getSent()
|
return new Sync(syncMessage.getSent()
|
||||||
.map(s -> Sent.from(s, recipientResolver, addressResolver, fileProvider)),
|
.map(s -> Sent.from(s, longTexts, recipientResolver, addressResolver, fileProvider)),
|
||||||
syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)),
|
syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)),
|
||||||
syncMessage.getRead()
|
syncMessage.getRead()
|
||||||
.map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
|
.map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
|
||||||
@ -582,6 +696,7 @@ public record MessageEnvelope(
|
|||||||
|
|
||||||
static Sent from(
|
static Sent from(
|
||||||
SentTranscriptMessage sentMessage,
|
SentTranscriptMessage sentMessage,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider
|
final AttachmentFileProvider fileProvider
|
||||||
@ -597,9 +712,17 @@ public record MessageEnvelope(
|
|||||||
.toApiRecipientAddress())
|
.toApiRecipientAddress())
|
||||||
.collect(Collectors.toSet()),
|
.collect(Collectors.toSet()),
|
||||||
sentMessage.getDataMessage()
|
sentMessage.getDataMessage()
|
||||||
.map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)),
|
.map(message -> Data.from(message,
|
||||||
|
longTexts,
|
||||||
|
recipientResolver,
|
||||||
|
addressResolver,
|
||||||
|
fileProvider)),
|
||||||
sentMessage.getEditMessage()
|
sentMessage.getEditMessage()
|
||||||
.map(message -> Edit.from(message, recipientResolver, addressResolver, fileProvider)),
|
.map(message -> Edit.from(message,
|
||||||
|
longTexts,
|
||||||
|
recipientResolver,
|
||||||
|
addressResolver,
|
||||||
|
fileProvider)),
|
||||||
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
|
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -616,7 +739,10 @@ public record MessageEnvelope(
|
|||||||
null,
|
null,
|
||||||
d.getE164(),
|
d.getE164(),
|
||||||
null))
|
null))
|
||||||
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
|
.toList(),
|
||||||
|
blockedListMessage.groups.stream()
|
||||||
|
.map(group -> GroupId.unknownVersion(group.getGroupId()))
|
||||||
|
.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -627,7 +753,7 @@ public record MessageEnvelope(
|
|||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver
|
RecipientAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
|
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSenderAci()))
|
||||||
.toApiRecipientAddress(), readMessage.getTimestamp());
|
.toApiRecipientAddress(), readMessage.getTimestamp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -898,23 +1024,24 @@ public record MessageEnvelope(
|
|||||||
public static MessageEnvelope from(
|
public static MessageEnvelope from(
|
||||||
SignalServiceEnvelope envelope,
|
SignalServiceEnvelope envelope,
|
||||||
SignalServiceContent content,
|
SignalServiceContent content,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider,
|
final AttachmentFileProvider fileProvider,
|
||||||
Exception exception
|
Exception exception
|
||||||
) {
|
) {
|
||||||
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
|
final var serviceId = envelope.getSourceServiceId();
|
||||||
final var source = !envelope.isUnidentifiedSender() && serviceId != null
|
final var source = !envelope.isUnidentifiedSender() && serviceId != null
|
||||||
? recipientResolver.resolveRecipient(serviceId)
|
? recipientResolver.resolveRecipient(serviceId)
|
||||||
: envelope.isUnidentifiedSender() && content != null
|
: envelope.isUnidentifiedSender() && content != null
|
||||||
? recipientResolver.resolveRecipient(content.getSender())
|
? recipientResolver.resolveRecipient(content.getSender())
|
||||||
: exception instanceof ProtocolException e
|
: exception instanceof ProtocolException e
|
||||||
? recipientResolver.resolveRecipient(e.getSender())
|
? recipientResolver.resolveRecipient(e.getSender())
|
||||||
: null;
|
: null;
|
||||||
final var sourceDevice = envelope.hasSourceDevice()
|
final var sourceDevice = envelope.hasSourceDevice()
|
||||||
? envelope.getSourceDevice()
|
? envelope.getSourceDevice()
|
||||||
: content != null
|
: content != null
|
||||||
? content.getSenderDevice()
|
? content.getSenderDevice()
|
||||||
: exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
|
: exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
|
||||||
|
|
||||||
Optional<Receipt> receipt;
|
Optional<Receipt> receipt;
|
||||||
@ -928,9 +1055,15 @@ public record MessageEnvelope(
|
|||||||
receipt = content.getReceiptMessage().map(Receipt::from);
|
receipt = content.getReceiptMessage().map(Receipt::from);
|
||||||
typing = content.getTypingMessage().map(Typing::from);
|
typing = content.getTypingMessage().map(Typing::from);
|
||||||
data = content.getDataMessage()
|
data = content.getDataMessage()
|
||||||
.map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider));
|
.map(dataMessage -> Data.from(dataMessage,
|
||||||
edit = content.getEditMessage().map(s -> Edit.from(s, recipientResolver, addressResolver, fileProvider));
|
longTexts,
|
||||||
sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider));
|
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));
|
||||||
call = content.getCallMessage().map(Call::from);
|
call = content.getCallMessage().map(Call::from);
|
||||||
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
|
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -10,12 +10,19 @@ public class ProofRequiredException extends Exception {
|
|||||||
|
|
||||||
private final String token;
|
private final String token;
|
||||||
private final Set<Option> options;
|
private final Set<Option> options;
|
||||||
private final long retryAfterSeconds;
|
private final long retryAfterMilliseconds;
|
||||||
|
|
||||||
public ProofRequiredException(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
|
public ProofRequiredException(final String token, final Set<Option> options, final long retryAfterMilliseconds) {
|
||||||
this.token = e.getToken();
|
super("Rate limit");
|
||||||
this.options = e.getOptions().stream().map(Option::from).collect(Collectors.toSet());
|
this.token = token;
|
||||||
this.retryAfterSeconds = e.getRetryAfterSeconds();
|
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 String getToken() {
|
public String getToken() {
|
||||||
@ -26,8 +33,8 @@ public class ProofRequiredException extends Exception {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getRetryAfterSeconds() {
|
public long getRetryAfterMilliseconds() {
|
||||||
return retryAfterSeconds;
|
return retryAfterMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Option {
|
public enum Option {
|
||||||
|
|||||||
@ -2,14 +2,18 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
public class RateLimitException extends Exception {
|
public class RateLimitException extends Exception {
|
||||||
|
|
||||||
private final long nextAttemptTimestamp;
|
private final Long retryAfterMilliseconds;
|
||||||
|
|
||||||
public RateLimitException(final long nextAttemptTimestamp) {
|
public RateLimitException(final Long retryAfterMilliseconds) {
|
||||||
super("Rate limit");
|
super("Rate limit");
|
||||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getNextAttemptTimestamp() {
|
public static RateLimitException from(org.whispersystems.signalservice.api.push.exceptions.RateLimitException e) {
|
||||||
return nextAttemptTimestamp;
|
return new RateLimitException(e.getRetryAfterMilliseconds().orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getRetryAfterMilliseconds() {
|
||||||
|
return retryAfterMilliseconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
public record ReceiveConfig(boolean ignoreAttachments, boolean ignoreStories, boolean sendReadReceipts) {}
|
public record ReceiveConfig(
|
||||||
|
boolean ignoreAttachments,
|
||||||
|
boolean ignoreStories,
|
||||||
|
boolean ignoreAvatars,
|
||||||
|
boolean ignoreStickers,
|
||||||
|
boolean sendReadReceipts
|
||||||
|
) {}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.signal.core.util.UuidUtil;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -24,13 +24,13 @@ public sealed interface RecipientIdentifier {
|
|||||||
sealed interface Single extends RecipientIdentifier {
|
sealed interface Single extends RecipientIdentifier {
|
||||||
|
|
||||||
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
|
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
|
||||||
if (UuidUtil.isUuid(identifier)) {
|
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
||||||
return new Uuid(UUID.fromString(identifier));
|
return new Uuid(UUID.fromString(identifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (identifier.startsWith("PNI:")) {
|
if (identifier.startsWith("PNI:")) {
|
||||||
final var pni = identifier.substring(4);
|
final var pni = identifier.substring(4);
|
||||||
if (!UuidUtil.isUuid(pni)) {
|
if (!UuidUtil.INSTANCE.isUuid(pni)) {
|
||||||
throw new InvalidNumberException("Invalid PNI");
|
throw new InvalidNumberException("Invalid PNI");
|
||||||
}
|
}
|
||||||
return new Pni(UUID.fromString(pni));
|
return new Pni(UUID.fromString(pni));
|
||||||
|
|||||||
@ -9,13 +9,13 @@ public record SendMessageResult(
|
|||||||
boolean isNetworkFailure,
|
boolean isNetworkFailure,
|
||||||
boolean isUnregisteredFailure,
|
boolean isUnregisteredFailure,
|
||||||
boolean isIdentityFailure,
|
boolean isIdentityFailure,
|
||||||
boolean isRateLimitFailure,
|
RateLimitException rateLimitException,
|
||||||
ProofRequiredException proofRequiredFailure,
|
ProofRequiredException proofRequiredFailure,
|
||||||
boolean isInvalidPreKeyFailure
|
boolean isInvalidPreKeyFailure
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static SendMessageResult unregisteredFailure(RecipientAddress address) {
|
public static SendMessageResult unregisteredFailure(RecipientAddress address) {
|
||||||
return new SendMessageResult(address, false, false, true, false, false, null, false);
|
return new SendMessageResult(address, false, false, true, false, null, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SendMessageResult from(
|
public static SendMessageResult from(
|
||||||
@ -23,16 +23,30 @@ public record SendMessageResult(
|
|||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver
|
RecipientAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
|
final var rateLimitFailure = sendMessageResult.getRateLimitFailure();
|
||||||
|
final var proofRequiredFailure = sendMessageResult.getProofRequiredFailure();
|
||||||
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
sendMessageResult.getAddress())).toApiRecipientAddress(),
|
sendMessageResult.getAddress())).toApiRecipientAddress(),
|
||||||
sendMessageResult.isSuccess(),
|
sendMessageResult.isSuccess(),
|
||||||
sendMessageResult.isNetworkFailure(),
|
sendMessageResult.isNetworkFailure(),
|
||||||
sendMessageResult.isUnregisteredFailure(),
|
sendMessageResult.isUnregisteredFailure(),
|
||||||
sendMessageResult.getIdentityFailure() != null,
|
sendMessageResult.getIdentityFailure() != null,
|
||||||
sendMessageResult.getRateLimitFailure() != null || sendMessageResult.getProofRequiredFailure() != null,
|
rateLimitFailure == null ? null : RateLimitException.from(rateLimitFailure),
|
||||||
sendMessageResult.getProofRequiredFailure() == null
|
proofRequiredFailure == null ? null : ProofRequiredException.from(proofRequiredFailure),
|
||||||
? null
|
|
||||||
: new ProofRequiredException(sendMessageResult.getProofRequiredFailure()),
|
|
||||||
sendMessageResult.isInvalidPreKeyFailure());
|
sendMessageResult.isInvalidPreKeyFailure());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRateLimitFailure() {
|
||||||
|
return this.rateLimitException != null || this.proofRequiredFailure != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long rateLimitRetryAfterMilliseconds() {
|
||||||
|
if (proofRequiredFailure != null) {
|
||||||
|
return proofRequiredFailure.getRetryAfterMilliseconds();
|
||||||
|
} else if (rateLimitException != null) {
|
||||||
|
return rateLimitException.getRetryAfterMilliseconds();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
|
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
|
||||||
|
|
||||||
@ -26,4 +27,18 @@ public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<S
|
|||||||
.flatMap(res -> res.stream().map(SendMessageResult::isRateLimitFailure))
|
.flatMap(res -> res.stream().map(SendMessageResult::isRateLimitFailure))
|
||||||
.allMatch(r -> r) && results.values().stream().mapToInt(List::size).sum() > 0;
|
.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,6 +1,6 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
import org.signal.core.util.Hex;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
import org.signal.core.util.Hex;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record TurnServer(
|
||||||
|
String username, String password, List<String> urls
|
||||||
|
) {}
|
||||||
@ -19,6 +19,8 @@ public class UpdateGroup {
|
|||||||
private final String avatarFile;
|
private final String avatarFile;
|
||||||
private final Integer expirationTimer;
|
private final Integer expirationTimer;
|
||||||
private final Boolean isAnnouncementGroup;
|
private final Boolean isAnnouncementGroup;
|
||||||
|
private final String labelEmoji;
|
||||||
|
private final String labelString;
|
||||||
|
|
||||||
private UpdateGroup(final Builder builder) {
|
private UpdateGroup(final Builder builder) {
|
||||||
name = builder.name;
|
name = builder.name;
|
||||||
@ -36,6 +38,8 @@ public class UpdateGroup {
|
|||||||
avatarFile = builder.avatarFile;
|
avatarFile = builder.avatarFile;
|
||||||
expirationTimer = builder.expirationTimer;
|
expirationTimer = builder.expirationTimer;
|
||||||
isAnnouncementGroup = builder.isAnnouncementGroup;
|
isAnnouncementGroup = builder.isAnnouncementGroup;
|
||||||
|
labelEmoji = builder.labelEmoji;
|
||||||
|
labelString = builder.labelString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder newBuilder() {
|
public static Builder newBuilder() {
|
||||||
@ -57,7 +61,9 @@ public class UpdateGroup {
|
|||||||
copy.editDetailsPermission,
|
copy.editDetailsPermission,
|
||||||
copy.avatarFile,
|
copy.avatarFile,
|
||||||
copy.expirationTimer,
|
copy.expirationTimer,
|
||||||
copy.isAnnouncementGroup);
|
copy.isAnnouncementGroup,
|
||||||
|
copy.labelEmoji,
|
||||||
|
copy.labelString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder newBuilder(
|
public static Builder newBuilder(
|
||||||
@ -75,7 +81,9 @@ public class UpdateGroup {
|
|||||||
final GroupPermission editDetailsPermission,
|
final GroupPermission editDetailsPermission,
|
||||||
final String avatarFile,
|
final String avatarFile,
|
||||||
final Integer expirationTimer,
|
final Integer expirationTimer,
|
||||||
final Boolean isAnnouncementGroup
|
final Boolean isAnnouncementGroup,
|
||||||
|
final String labelEmoji,
|
||||||
|
final String labelString
|
||||||
) {
|
) {
|
||||||
return new Builder(name,
|
return new Builder(name,
|
||||||
description,
|
description,
|
||||||
@ -91,7 +99,9 @@ public class UpdateGroup {
|
|||||||
editDetailsPermission,
|
editDetailsPermission,
|
||||||
avatarFile,
|
avatarFile,
|
||||||
expirationTimer,
|
expirationTimer,
|
||||||
isAnnouncementGroup);
|
isAnnouncementGroup,
|
||||||
|
labelEmoji,
|
||||||
|
labelString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@ -154,6 +164,14 @@ public class UpdateGroup {
|
|||||||
return isAnnouncementGroup;
|
return isAnnouncementGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getLabelEmoji() {
|
||||||
|
return labelEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabelString() {
|
||||||
|
return labelString;
|
||||||
|
}
|
||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
@ -171,6 +189,8 @@ public class UpdateGroup {
|
|||||||
private String avatarFile;
|
private String avatarFile;
|
||||||
private Integer expirationTimer;
|
private Integer expirationTimer;
|
||||||
private Boolean isAnnouncementGroup;
|
private Boolean isAnnouncementGroup;
|
||||||
|
private String labelEmoji;
|
||||||
|
private String labelString;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
}
|
}
|
||||||
@ -190,7 +210,9 @@ public class UpdateGroup {
|
|||||||
final GroupPermission editDetailsPermission,
|
final GroupPermission editDetailsPermission,
|
||||||
final String avatarFile,
|
final String avatarFile,
|
||||||
final Integer expirationTimer,
|
final Integer expirationTimer,
|
||||||
final Boolean isAnnouncementGroup
|
final Boolean isAnnouncementGroup,
|
||||||
|
final String labelEmoji,
|
||||||
|
final String labelString
|
||||||
) {
|
) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
@ -207,6 +229,8 @@ public class UpdateGroup {
|
|||||||
this.avatarFile = avatarFile;
|
this.avatarFile = avatarFile;
|
||||||
this.expirationTimer = expirationTimer;
|
this.expirationTimer = expirationTimer;
|
||||||
this.isAnnouncementGroup = isAnnouncementGroup;
|
this.isAnnouncementGroup = isAnnouncementGroup;
|
||||||
|
this.labelEmoji = labelEmoji;
|
||||||
|
this.labelString = labelString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withName(final String val) {
|
public Builder withName(final String val) {
|
||||||
@ -284,6 +308,16 @@ public class UpdateGroup {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withLabelEmoji(final String val) {
|
||||||
|
labelEmoji = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withLabelString(final String val) {
|
||||||
|
labelString = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public UpdateGroup build() {
|
public UpdateGroup build() {
|
||||||
return new UpdateGroup(this);
|
return new UpdateGroup(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.signal.core.util.Base64;
|
import org.signal.core.util.Base64;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -36,7 +36,7 @@ public final class UsernameLinkUrl {
|
|||||||
|
|
||||||
final var entropy = Arrays.copyOfRange(allBytes, 0, 32);
|
final var entropy = Arrays.copyOfRange(allBytes, 0, 32);
|
||||||
final var serverId = Arrays.copyOfRange(allBytes, 32, allBytes.length);
|
final var serverId = Arrays.copyOfRange(allBytes, 32, allBytes.length);
|
||||||
final var serverIdUuid = UuidUtil.parseOrNull(serverId);
|
final var serverIdUuid = UuidUtil.INSTANCE.parseOrNull(serverId);
|
||||||
if (serverIdUuid == null) {
|
if (serverIdUuid == null) {
|
||||||
throw new InvalidUsernameLinkException("Invalid serverId");
|
throw new InvalidUsernameLinkException("Invalid serverId");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,15 @@ package org.asamk.signal.manager.config;
|
|||||||
import org.signal.libsignal.net.Network.Environment;
|
import org.signal.libsignal.net.Network.Environment;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
import org.signal.network.config.HttpProxy;
|
||||||
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
import org.signal.network.config.SignalCdnUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
import org.signal.network.config.SignalCdsiUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
import org.signal.network.config.SignalProxy;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
import org.signal.network.config.SignalServiceConfiguration;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
import org.signal.network.config.SignalServiceUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
import org.signal.network.config.SignalStorageUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
import org.signal.network.config.SignalSvr2Url;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
import org.signal.network.config.TrustStore;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -30,8 +30,8 @@ class LiveConfig {
|
|||||||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
||||||
.decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6");
|
.decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6");
|
||||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||||
private static final String SVR2_MRENCLAVE_LEGACY = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6";
|
private static final String SVR2_MRENCLAVE = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4";
|
||||||
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
|
private static final String SVR2_MRENCLAVE_LEGACY = "1240acbd4aa26974184844c8a46b1022d3957ac8a76c1fd8f5b1a15141ee0708";
|
||||||
|
|
||||||
private static final String URL = "https://chat.signal.org";
|
private static final String URL = "https://chat.signal.org";
|
||||||
private static final String CDN_URL = "https://cdn.signal.org";
|
private static final String CDN_URL = "https://cdn.signal.org";
|
||||||
|
|||||||
@ -20,6 +20,7 @@ public class ServiceConfig {
|
|||||||
|
|
||||||
public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
|
public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
|
||||||
public static final long MAX_ENVELOPE_SIZE = 0;
|
public static final long MAX_ENVELOPE_SIZE = 0;
|
||||||
|
public static final int MAX_INCREMENTAL_MACS_PER_ENVELOPE = 10;
|
||||||
public static final int MAX_MESSAGE_SIZE_BYTES = 2000;
|
public static final int MAX_MESSAGE_SIZE_BYTES = 2000;
|
||||||
public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
|
public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
|
||||||
public static final boolean AUTOMATIC_NETWORK_RETRY = true;
|
public static final boolean AUTOMATIC_NETWORK_RETRY = true;
|
||||||
@ -29,8 +30,15 @@ public class ServiceConfig {
|
|||||||
|
|
||||||
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
||||||
final var attachmentBackfill = !isPrimaryDevice;
|
final var attachmentBackfill = !isPrimaryDevice;
|
||||||
final var spqr = !isPrimaryDevice;
|
final var spqr = true;
|
||||||
return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr);
|
final var usernameSyncChangeMessage = !isPrimaryDevice;
|
||||||
|
final var optionalPhoneNumber = !isPrimaryDevice;
|
||||||
|
return new AccountAttributes.Capabilities(true,
|
||||||
|
true,
|
||||||
|
attachmentBackfill,
|
||||||
|
spqr,
|
||||||
|
usernameSyncChangeMessage,
|
||||||
|
optionalPhoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package org.asamk.signal.manager.config;
|
|||||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
import org.signal.network.config.SignalServiceConfiguration;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,15 @@ package org.asamk.signal.manager.config;
|
|||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
import org.signal.network.config.HttpProxy;
|
||||||
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
import org.signal.network.config.SignalCdnUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
import org.signal.network.config.SignalCdsiUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
import org.signal.network.config.SignalProxy;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
import org.signal.network.config.SignalServiceConfiguration;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
import org.signal.network.config.SignalServiceUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
import org.signal.network.config.SignalStorageUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
import org.signal.network.config.SignalSvr2Url;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
import org.signal.network.config.TrustStore;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -30,8 +30,8 @@ class StagingConfig {
|
|||||||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
||||||
.decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm");
|
.decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm");
|
||||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||||
private static final String SVR2_MRENCLAVE_LEGACY = "2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91";
|
private static final String SVR2_MRENCLAVE = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302";
|
||||||
private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036";
|
private static final String SVR2_MRENCLAVE_LEGACY = "97f151f6ed078edbbfd72fa9cae694dcc08353f1f5e8d9ccd79a971b10ffc535";
|
||||||
|
|
||||||
private static final String URL = "https://chat.staging.signal.org";
|
private static final String URL = "https://chat.staging.signal.org";
|
||||||
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.asamk.signal.manager.config;
|
package org.asamk.signal.manager.config;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
import org.signal.network.config.TrustStore;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
|
||||||
public interface AccountFileUpdater {
|
public interface AccountFileUpdater {
|
||||||
|
|
||||||
|
|||||||
@ -14,22 +14,26 @@ import org.asamk.signal.manager.storage.SignalAccount;
|
|||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
import org.signal.core.util.Base64;
|
import org.signal.core.util.Base64;
|
||||||
|
import org.signal.core.util.crypto.DeviceNameCipher;
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
|
import org.signal.libsignal.protocol.NoSessionException;
|
||||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
||||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||||
import org.signal.libsignal.protocol.util.KeyHelper;
|
import org.signal.libsignal.protocol.util.KeyHelper;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.signal.libsignal.usernames.Username;
|
import org.signal.libsignal.usernames.Username;
|
||||||
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
|
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||||
@ -37,10 +41,6 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
|||||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
|
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
|
||||||
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
|
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
|
||||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
|
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
|
||||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||||
@ -48,7 +48,9 @@ import org.whispersystems.signalservice.internal.push.SyncMessage;
|
|||||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -59,6 +61,7 @@ import okio.ByteString;
|
|||||||
|
|
||||||
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
|
||||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||||
|
|
||||||
public class AccountHelper {
|
public class AccountHelper {
|
||||||
@ -82,16 +85,9 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void checkAccountState() throws IOException {
|
public void checkAccountState() throws IOException {
|
||||||
if (account.getLastReceiveTimestamp() == 0) {
|
if (account.getAci() == null) {
|
||||||
logger.info("The Signal protocol expects that incoming messages are regularly received.");
|
account.setRegistered(false);
|
||||||
} else {
|
throw new IOException("Account without ACI");
|
||||||
var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
|
|
||||||
long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
|
|
||||||
if (days > 7) {
|
|
||||||
logger.warn(
|
|
||||||
"Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
|
|
||||||
days);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
updateAccountAttributes();
|
updateAccountAttributes();
|
||||||
@ -100,7 +96,7 @@ public class AccountHelper {
|
|||||||
} else {
|
} else {
|
||||||
context.getPreKeyHelper().refreshPreKeysIfNecessary();
|
context.getPreKeyHelper().refreshPreKeysIfNecessary();
|
||||||
}
|
}
|
||||||
if (account.getAci() == null || account.getPni() == null) {
|
if (account.getPni() == null) {
|
||||||
checkWhoAmiI();
|
checkWhoAmiI();
|
||||||
}
|
}
|
||||||
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
||||||
@ -125,6 +121,17 @@ public class AccountHelper {
|
|||||||
account.setRegistered(false);
|
account.setRegistered(false);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
if (account.getLastReceiveTimestamp() == 0) {
|
||||||
|
logger.info("The Signal protocol expects that incoming messages are regularly received.");
|
||||||
|
} else {
|
||||||
|
var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
|
||||||
|
long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
|
||||||
|
if (days > 7) {
|
||||||
|
logger.warn(
|
||||||
|
"Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
|
||||||
|
days);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkWhoAmiI() throws IOException {
|
public void checkWhoAmiI() throws IOException {
|
||||||
@ -278,13 +285,16 @@ public class AccountHelper {
|
|||||||
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
|
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
|
||||||
pniChangeNumber);
|
pniChangeNumber);
|
||||||
encryptedDeviceMessages.add(message);
|
encryptedDeviceMessages.add(message);
|
||||||
} catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
|
} catch (UntrustedIdentityException | IOException | InvalidKeyException | NoSessionException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var sessionId = account.getSessionId(newNumber);
|
final var sessionId = account.getSessionId(newNumber);
|
||||||
|
if (sessionId == null) {
|
||||||
|
throw new IOException("No change number verification session active");
|
||||||
|
}
|
||||||
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
pin,
|
pin,
|
||||||
@ -308,16 +318,21 @@ public class AccountHelper {
|
|||||||
Utils.mapKeys(pniRegistrationIds, Object::toString))));
|
Utils.mapKeys(pniRegistrationIds, Object::toString))));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
account.clearSessionId();
|
||||||
final var updatePni = PNI.parseOrThrow(result.first().getPni());
|
final var updatePni = PNI.parseOrThrow(result.first().getPni());
|
||||||
if (updatePni.equals(account.getPni())) {
|
if (updatePni.equals(account.getPni())) {
|
||||||
logger.debug("PNI is unchanged after change number");
|
logger.debug("PNI is unchanged after change number");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePniChangeNumberMessage(selfChangeNumber, updatePni);
|
handlePniChangeNumberMessage(selfChangeNumber, updatePni, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
|
public boolean handlePniChangeNumberMessage(
|
||||||
|
final SyncMessage.PniChangeNumber pniChangeNumber,
|
||||||
|
final PNI updatedPni,
|
||||||
|
final boolean forcePniPreKeyRotation
|
||||||
|
) {
|
||||||
if (pniChangeNumber.identityKeyPair != null
|
if (pniChangeNumber.identityKeyPair != null
|
||||||
&& pniChangeNumber.registrationId != null
|
&& pniChangeNumber.registrationId != null
|
||||||
&& pniChangeNumber.signedPreKey != null) {
|
&& pniChangeNumber.signedPreKey != null) {
|
||||||
@ -331,10 +346,19 @@ public class AccountHelper {
|
|||||||
pniChangeNumber.lastResortKyberPreKey != null
|
pniChangeNumber.lastResortKyberPreKey != null
|
||||||
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
|
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
|
||||||
: null);
|
: null);
|
||||||
|
if (forcePniPreKeyRotation) {
|
||||||
|
try {
|
||||||
|
context.getPreKeyHelper().forceRefreshPreKeys(ServiceIdType.PNI);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to force refresh PNI pre keys after PNI change sync", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed to handle change number message", e);
|
logger.warn("Failed to handle change number message", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final int USERNAME_MIN_LENGTH = 3;
|
public static final int USERNAME_MIN_LENGTH = 3;
|
||||||
@ -376,13 +400,17 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void reserveUsername(final List<Username> candidates) throws IOException {
|
private void reserveUsername(final List<Username> candidates) throws IOException {
|
||||||
final var candidateHashes = new ArrayList<String>();
|
final var candidateHashes = new ArrayList<byte[]>();
|
||||||
for (final var candidate : candidates) {
|
for (final var candidate : candidates) {
|
||||||
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
|
candidateHashes.add(candidate.getHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
final var usernameHash = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||||
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
final var hashIndex = candidateHashes.stream()
|
||||||
|
.filter(candidateHash -> Arrays.equals(candidateHash, usernameHash))
|
||||||
|
.findFirst()
|
||||||
|
.map(candidateHashes::indexOf)
|
||||||
|
.orElse(-1);
|
||||||
if (hashIndex == -1) {
|
if (hashIndex == -1) {
|
||||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||||
throw new IOException("Unexpected username response");
|
throw new IOException("Unexpected username response");
|
||||||
@ -457,7 +485,7 @@ public class AccountHelper {
|
|||||||
logger.debug("Attempting to resynchronize username.");
|
logger.debug("Attempting to resynchronize username.");
|
||||||
try {
|
try {
|
||||||
tryReserveConfirmUsername(username);
|
tryReserveConfirmUsername(username);
|
||||||
} catch (UsernameMalformedException | UsernameTakenException | UsernameIsNotReservedException e) {
|
} catch (NonSuccessfulResponseCodeException e) {
|
||||||
logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
|
logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
|
||||||
e.getMessage(),
|
e.getMessage(),
|
||||||
e.getClass().getSimpleName());
|
e.getClass().getSimpleName());
|
||||||
@ -475,8 +503,7 @@ public class AccountHelper {
|
|||||||
final var usernameLink = account.getUsernameLink();
|
final var usernameLink = account.getUsernameLink();
|
||||||
|
|
||||||
if (usernameLink == null) {
|
if (usernameLink == null) {
|
||||||
handleResponseException(dependencies.getAccountApi()
|
handleResponseException(dependencies.getAccountApi().reserveUsername(List.of(username.getHash())));
|
||||||
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
|
|
||||||
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
||||||
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||||
account.setUsernameLink(linkComponents);
|
account.setUsernameLink(linkComponents);
|
||||||
@ -501,17 +528,39 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public void deleteUsername() throws IOException {
|
public void deleteUsername() throws IOException {
|
||||||
handleResponseException(dependencies.getAccountApi().deleteUsername());
|
handleResponseException(dependencies.getAccountApi().deleteUsernameHash());
|
||||||
account.setUsernameLink(null);
|
account.setUsernameLink(null);
|
||||||
account.setUsername(null);
|
account.setUsername(null);
|
||||||
logger.debug("[deleteUsername] Successfully deleted the username.");
|
logger.debug("[deleteUsername] Successfully deleted the username.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeviceName(String deviceName) {
|
public void setDeviceName(String deviceName) {
|
||||||
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
||||||
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
account.setEncryptedDeviceName(Base64.encodeWithoutPadding(encryptedDeviceName));
|
||||||
account.setEncryptedDeviceName(encryptedDeviceName);
|
}
|
||||||
|
|
||||||
|
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
||||||
|
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
||||||
|
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||||
|
.setDeviceName(encryptedDeviceName, deviceId, cont));
|
||||||
|
context.getSyncHelper().sendDeviceNameChange(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getEncryptedDeviceName(final String deviceName) {
|
||||||
|
final var identityKey = account.getAciIdentityKeyPair();
|
||||||
|
return DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), identityKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshDeviceName() throws IOException {
|
||||||
|
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||||
|
.getDevices(cont));
|
||||||
|
final var deviceId = account.getDeviceId();
|
||||||
|
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
|
||||||
|
if (device.isPresent()) {
|
||||||
|
account.setEncryptedDeviceName(device.get().name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateAccountAttributes() throws IOException {
|
public void updateAccountAttributes() throws IOException {
|
||||||
@ -540,14 +589,16 @@ public class AccountHelper {
|
|||||||
account.getOrCreatePinMasterKey(),
|
account.getOrCreatePinMasterKey(),
|
||||||
account.getOrCreateMediaRootBackupKey(),
|
account.getOrCreateMediaRootBackupKey(),
|
||||||
verificationCode.getVerificationCode(),
|
verificationCode.getVerificationCode(),
|
||||||
null));
|
null,
|
||||||
|
account.getAuthCredentialSalt()));
|
||||||
account.setMultiDevice(true);
|
account.setMultiDevice(true);
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeLinkedDevices(int deviceId) throws IOException {
|
public void removeLinkedDevices(int deviceId) throws IOException {
|
||||||
handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
|
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi().removeDevice(deviceId, cont));
|
||||||
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||||
|
.getDevices(cont));
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,16 +606,14 @@ public class AccountHelper {
|
|||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
||||||
handleResponseException(dependencies.getAccountApi()
|
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
||||||
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegistrationPin(String pin) throws IOException {
|
public void setRegistrationPin(String pin) throws IOException {
|
||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
||||||
handleResponseException(dependencies.getAccountApi()
|
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
||||||
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
|
||||||
|
|
||||||
account.setRegistrationLockPin(pin);
|
account.setRegistrationLockPin(pin);
|
||||||
updateAccountAttributes();
|
updateAccountAttributes();
|
||||||
@ -582,7 +631,7 @@ public class AccountHelper {
|
|||||||
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
||||||
// If this is the primary device, other users can't send messages to this number anymore.
|
// If this is the primary device, other users can't send messages to this number anymore.
|
||||||
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
||||||
handleResponseException(dependencies.getAccountApi().clearFcmToken());
|
handleResponseExceptionSuspend(cont -> dependencies.getAccountApi().clearFcmToken(cont));
|
||||||
|
|
||||||
account.setRegistered(false);
|
account.setRegistered(false);
|
||||||
unregisteredListener.call();
|
unregisteredListener.call();
|
||||||
|
|||||||
@ -6,15 +6,19 @@ import org.asamk.signal.manager.internal.SignalDependencies;
|
|||||||
import org.asamk.signal.manager.storage.AttachmentStore;
|
import org.asamk.signal.manager.storage.AttachmentStore;
|
||||||
import org.asamk.signal.manager.util.AttachmentUtils;
|
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
|
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||||
|
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -30,8 +34,10 @@ public class AttachmentHelper {
|
|||||||
|
|
||||||
private final SignalDependencies dependencies;
|
private final SignalDependencies dependencies;
|
||||||
private final AttachmentStore attachmentStore;
|
private final AttachmentStore attachmentStore;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
public AttachmentHelper(final Context context) {
|
public AttachmentHelper(final Context context) {
|
||||||
|
this.context = context;
|
||||||
this.dependencies = context.getDependencies();
|
this.dependencies = context.getDependencies();
|
||||||
this.attachmentStore = context.getAttachmentStore();
|
this.attachmentStore = context.getAttachmentStore();
|
||||||
}
|
}
|
||||||
@ -44,8 +50,11 @@ public class AttachmentHelper {
|
|||||||
return attachmentStore.retrieveAttachment(id);
|
return attachmentStore.retrieveAttachment(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
|
public List<SignalServiceAttachment> uploadAttachments(
|
||||||
final var attachmentStreams = createAttachmentStreams(attachments);
|
final List<String> attachments,
|
||||||
|
boolean voiceNote
|
||||||
|
) throws AttachmentInvalidException, IOException {
|
||||||
|
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload attachments here, so we only upload once even for multiple recipients
|
// Upload attachments here, so we only upload once even for multiple recipients
|
||||||
@ -61,21 +70,67 @@ public class AttachmentHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException {
|
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
|
||||||
|
return uploadAttachments(attachments, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SignalServiceAttachmentStream> createAttachmentStreams(
|
||||||
|
List<String> attachments,
|
||||||
|
boolean voiceNote
|
||||||
|
) throws AttachmentInvalidException, IOException {
|
||||||
if (attachments == null) {
|
if (attachments == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
||||||
for (var attachment : attachments) {
|
for (var attachment : attachments) {
|
||||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
final var attachmentStream = getAttachmentStream(attachment, voiceNote);
|
||||||
signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, uploadSpec));
|
signalServiceAttachments.add(attachmentStream);
|
||||||
}
|
}
|
||||||
return signalServiceAttachments;
|
return signalServiceAttachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SignalServiceAttachmentStream getAttachmentStream(
|
||||||
|
final String attachment,
|
||||||
|
final boolean voiceNote
|
||||||
|
) throws AttachmentInvalidException {
|
||||||
|
try {
|
||||||
|
// Reject local files that point into the signal-cli data directory
|
||||||
|
if (attachment != null && !attachment.startsWith("data:")) {
|
||||||
|
try {
|
||||||
|
final var file = new File(attachment);
|
||||||
|
final var canonical = file.getCanonicalFile();
|
||||||
|
final var dataPath = context.getAccount().getDataPath().getCanonicalFile();
|
||||||
|
if (canonical.toPath().startsWith(dataPath.toPath())) {
|
||||||
|
throw new AttachmentInvalidException(attachment,
|
||||||
|
new IOException("Attaching files from the signal-cli data directory is not allowed"));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AttachmentInvalidException(attachment, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var streamDetailsAndFileName = Utils.createStreamDetails(attachment);
|
||||||
|
final var streamDetails = streamDetailsAndFileName.first();
|
||||||
|
final var uploadSpec = getResumableUploadSpec(streamDetails);
|
||||||
|
|
||||||
|
return AttachmentUtils.createAttachmentStream(streamDetails,
|
||||||
|
streamDetailsAndFileName.second(),
|
||||||
|
voiceNote,
|
||||||
|
uploadSpec);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AttachmentInvalidException(attachment, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResumableUploadSpec getResumableUploadSpec(final StreamDetails streamDetails) throws IOException {
|
||||||
|
final var streamLength = streamDetails.getLength();
|
||||||
|
final var ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(
|
||||||
|
streamLength));
|
||||||
|
return dependencies.getCdnService().getResumableUploadSpecBlocking(ciphertextLength);
|
||||||
|
}
|
||||||
|
|
||||||
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
||||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
final var attachmentStream = getAttachmentStream(attachment, false);
|
||||||
var attachmentStream = AttachmentUtils.createAttachmentStream(attachment, uploadSpec);
|
|
||||||
return uploadAttachment(attachmentStream);
|
return uploadAttachment(attachmentStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,809 @@
|
|||||||
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.Manager;
|
||||||
|
import org.asamk.signal.manager.api.CallInfo;
|
||||||
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
import org.asamk.signal.manager.api.TurnServer;
|
||||||
|
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||||
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.asamk.signal.manager.util.Utils;
|
||||||
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.asamk.signal.manager.util.Utils.callIdUnsigned;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages active voice calls: tracks state, spawns/monitors the signal-call-tunnel
|
||||||
|
* subprocess, routes incoming call messages, and handles timeouts.
|
||||||
|
*/
|
||||||
|
public class CallManager implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CallManager.class);
|
||||||
|
private static final long RING_TIMEOUT_MS = 60_000;
|
||||||
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final SignalAccount account;
|
||||||
|
private final SignalDependencies dependencies;
|
||||||
|
private final Map<Long, CallState> activeCalls = new ConcurrentHashMap<>();
|
||||||
|
private final List<Manager.CallEventListener> callEventListeners = new CopyOnWriteArrayList<>();
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
|
var t = new Thread(r, "call-timeout-scheduler");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
public CallManager(final Context context) {
|
||||||
|
this.context = context;
|
||||||
|
this.account = context.getAccount();
|
||||||
|
this.dependencies = context.getDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCallEventListener(Manager.CallEventListener listener) {
|
||||||
|
callEventListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCallEventListener(Manager.CallEventListener listener) {
|
||||||
|
callEventListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireCallEvent(CallState state, String reason) {
|
||||||
|
var callInfo = state.toCallInfo(account.getRecipientAddressResolver());
|
||||||
|
for (var listener : callEventListeners) {
|
||||||
|
try {
|
||||||
|
listener.handleCallEvent(callInfo, reason);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.warn("Call event listener failed, ignoring", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallInfo startOutgoingCall(
|
||||||
|
final RecipientId recipientId
|
||||||
|
) throws IOException {
|
||||||
|
var callId = generateCallId();
|
||||||
|
var recipientAddress = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
|
||||||
|
|
||||||
|
var state = new CallState(callId, CallInfo.State.RINGING_OUTGOING, recipientId, null, true);
|
||||||
|
logger.debug("Starting outgoing call {} to {} (recipientId: {})",
|
||||||
|
callIdUnsigned(callId),
|
||||||
|
recipientAddress,
|
||||||
|
recipientId);
|
||||||
|
activeCalls.put(callId, state);
|
||||||
|
dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
|
||||||
|
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
|
||||||
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
|
// Spawn call tunnel binary and connect control channel
|
||||||
|
spawnMediaTunnel(state);
|
||||||
|
|
||||||
|
// Fetch TURN servers
|
||||||
|
var turnServers = getTurnServers();
|
||||||
|
|
||||||
|
// Send createOutgoingCall + proceed via control channel
|
||||||
|
var createMsg = mapper.createObjectNode();
|
||||||
|
createMsg.put("type", "createOutgoingCall");
|
||||||
|
createMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||||
|
createMsg.put("peerId", recipientAddress.toString());
|
||||||
|
sendControlMessage(state, writeJson(createMsg));
|
||||||
|
sendProceed(state, callId, turnServers);
|
||||||
|
|
||||||
|
// Schedule ring timeout
|
||||||
|
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
logger.debug("Started outgoing call {} to {}", callIdUnsigned(callId), recipientAddress);
|
||||||
|
return state.toCallInfo(account.getRecipientAddressResolver());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallInfo acceptIncomingCall(final long callId) throws IOException {
|
||||||
|
final var state = getActiveCall(callId);
|
||||||
|
if (state.state != CallInfo.State.RINGING_INCOMING) {
|
||||||
|
throw new IOException("Call "
|
||||||
|
+ callId
|
||||||
|
+ " is not in RINGING_INCOMING state (current: "
|
||||||
|
+ state.state
|
||||||
|
+ ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer the accept until the tunnel reports Ringing state.
|
||||||
|
// Sending accept too early (while RingRTC is in ConnectingBeforeAccepted)
|
||||||
|
// causes it to be silently dropped.
|
||||||
|
state.acceptPending = true;
|
||||||
|
// If the tunnel is already in Ringing state, send immediately
|
||||||
|
sendAcceptIfReady(state);
|
||||||
|
|
||||||
|
state.state = CallInfo.State.CONNECTING;
|
||||||
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
|
logger.debug("Accepted incoming call {}", callIdUnsigned(callId));
|
||||||
|
return state.toCallInfo(account.getRecipientAddressResolver());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hangupCall(final long callId) throws IOException {
|
||||||
|
getActiveCall(callId);
|
||||||
|
endCall(callId, "local_hangup");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendMessageResult rejectCall(final long callId) throws IOException {
|
||||||
|
final var callState = getActiveCall(callId);
|
||||||
|
|
||||||
|
final var result = sendBusyMessage(callState.callId, callState.recipientId, callState.deviceId);
|
||||||
|
|
||||||
|
endCall(callId, "rejected");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CallInfo> listActiveCalls() {
|
||||||
|
return activeCalls.values()
|
||||||
|
.stream()
|
||||||
|
.map((CallState callState) -> callState.toCallInfo(account.getRecipientAddressResolver()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TurnServer> getTurnServers() throws IOException {
|
||||||
|
try {
|
||||||
|
var turnServerList = handleResponseException(dependencies.getCallingApi().getTurnServerInfo());
|
||||||
|
return turnServerList.stream()
|
||||||
|
.map(info -> new TurnServer(info.getUsername(), info.getPassword(), info.getUrls()))
|
||||||
|
.toList();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.warn("Failed to get TURN server info, returning empty list", e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Incoming call message handling ---
|
||||||
|
|
||||||
|
public void handleIncomingOffer(
|
||||||
|
final RecipientId recipientId,
|
||||||
|
final int deviceId,
|
||||||
|
final long callId,
|
||||||
|
final MessageEnvelope.Call.Offer.Type type,
|
||||||
|
final byte[] opaque
|
||||||
|
) {
|
||||||
|
if (callEventListeners.isEmpty()) {
|
||||||
|
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
|
||||||
|
callIdUnsigned(callId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var senderAddress = account.getRecipientAddressResolver()
|
||||||
|
.resolveRecipientAddress(recipientId)
|
||||||
|
.toApiRecipientAddress();
|
||||||
|
|
||||||
|
logger.debug("Incoming offer opaque ({} bytes)", opaque == null ? 0 : opaque.length);
|
||||||
|
|
||||||
|
var state = new CallState(callId, CallInfo.State.RINGING_INCOMING, recipientId, deviceId, false);
|
||||||
|
logger.debug("Starting incoming call {} from {} (recipientId: {})",
|
||||||
|
callIdUnsigned(callId),
|
||||||
|
senderAddress,
|
||||||
|
recipientId);
|
||||||
|
activeCalls.put(callId, state);
|
||||||
|
|
||||||
|
// Spawn call tunnel binary immediately
|
||||||
|
spawnMediaTunnel(state);
|
||||||
|
|
||||||
|
// Get identity keys for the receivedOffer message
|
||||||
|
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
|
||||||
|
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||||
|
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
|
||||||
|
|
||||||
|
// Fetch TURN servers
|
||||||
|
List<TurnServer> turnServers;
|
||||||
|
try {
|
||||||
|
turnServers = getTurnServers();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to get TURN servers for incoming call {}", callIdUnsigned(callId), e);
|
||||||
|
turnServers = List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send receivedOffer to subprocess
|
||||||
|
var offerMsg = mapper.createObjectNode();
|
||||||
|
offerMsg.put("type", "receivedOffer");
|
||||||
|
offerMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||||
|
offerMsg.put("peerId", senderAddress.toString());
|
||||||
|
offerMsg.put("senderDeviceId", deviceId);
|
||||||
|
offerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
|
||||||
|
offerMsg.put("age", 0);
|
||||||
|
offerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
|
||||||
|
offerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
|
||||||
|
sendControlMessage(state, writeJson(offerMsg));
|
||||||
|
|
||||||
|
// Send proceed with TURN servers
|
||||||
|
sendProceed(state, callId, turnServers);
|
||||||
|
|
||||||
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
|
// Schedule ring timeout
|
||||||
|
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
logger.debug("Incoming call {} from {}", callIdUnsigned(callId), senderAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingAnswer(final long callId, final int deviceId, final byte[] opaque) {
|
||||||
|
var state = activeCalls.get(callId);
|
||||||
|
if (state == null) {
|
||||||
|
logger.warn("Received answer for unknown call {}", callIdUnsigned(callId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get identity keys
|
||||||
|
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
|
||||||
|
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||||
|
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
|
||||||
|
|
||||||
|
// Forward raw opaque to subprocess
|
||||||
|
var answerMsg = mapper.createObjectNode();
|
||||||
|
answerMsg.put("type", "receivedAnswer");
|
||||||
|
answerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
|
||||||
|
answerMsg.put("senderDeviceId", deviceId);
|
||||||
|
answerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
|
||||||
|
answerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
|
||||||
|
sendControlMessage(state, writeJson(answerMsg));
|
||||||
|
|
||||||
|
state.deviceId = deviceId;
|
||||||
|
state.state = CallInfo.State.CONNECTING;
|
||||||
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
|
logger.debug("Received answer for call {}", callIdUnsigned(callId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingIceCandidate(final long callId, final byte[] opaque, final int deviceId) {
|
||||||
|
var state = activeCalls.get(callId);
|
||||||
|
if (state == null) {
|
||||||
|
logger.debug("Received ICE candidate for unknown call {}", callIdUnsigned(callId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to subprocess as receivedIce
|
||||||
|
var iceMsg = mapper.createObjectNode();
|
||||||
|
iceMsg.put("type", "receivedIce");
|
||||||
|
iceMsg.put("senderDeviceId", deviceId);
|
||||||
|
var candidates = iceMsg.putArray("candidates");
|
||||||
|
candidates.add(java.util.Base64.getEncoder().encodeToString(opaque));
|
||||||
|
sendControlMessage(state, writeJson(iceMsg));
|
||||||
|
logger.debug("Forwarded ICE candidate to tunnel for call {}", callIdUnsigned(callId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingHangup(final long callId) {
|
||||||
|
if (callEventListeners.isEmpty() && !activeCalls.containsKey(callId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
endCall(callId, "remote_hangup");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingBusy(final long callId) {
|
||||||
|
if (callEventListeners.isEmpty() && !activeCalls.containsKey(callId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
endCall(callId, "remote_busy");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal helpers ---
|
||||||
|
|
||||||
|
private CallState getActiveCall(final long callId) throws IOException {
|
||||||
|
var state = activeCalls.get(callId);
|
||||||
|
if (state == null) {
|
||||||
|
throw new IOException("No active call with id " + callIdUnsigned(callId));
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendBusyMessage(final long callId, final RecipientId recipientId, final int deviceId) {
|
||||||
|
var busyMessage = new BusyMessage(callId);
|
||||||
|
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, deviceId);
|
||||||
|
return context.getSendHelper().sendCallMessage(callMessage, recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendControlMessage(CallState state, String json) {
|
||||||
|
if (state.controlWriter == null) {
|
||||||
|
logger.debug("Queueing control message for call {} (not yet connected): {}",
|
||||||
|
callIdUnsigned(state.callId),
|
||||||
|
json);
|
||||||
|
state.pendingControlMessages.add(json);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.controlWriter.println(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendProceed(CallState state, long callId, List<TurnServer> turnServers) {
|
||||||
|
var proceedMsg = mapper.createObjectNode();
|
||||||
|
proceedMsg.put("type", "proceed");
|
||||||
|
proceedMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||||
|
proceedMsg.put("hideIp", false);
|
||||||
|
var iceServers = proceedMsg.putArray("iceServers");
|
||||||
|
for (var ts : turnServers) {
|
||||||
|
var server = iceServers.addObject();
|
||||||
|
server.put("username", ts.username());
|
||||||
|
server.put("password", ts.password());
|
||||||
|
var urls = server.putArray("urls");
|
||||||
|
for (var url : ts.urls()) {
|
||||||
|
urls.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendControlMessage(state, writeJson(proceedMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnMediaTunnel(CallState state) {
|
||||||
|
try {
|
||||||
|
var command = new ArrayList<>(List.of(findTunnelBinary()));
|
||||||
|
|
||||||
|
var processBuilder = new ProcessBuilder(command);
|
||||||
|
// Keep stdout and stderr separate: stdout = control protocol, stderr = logging
|
||||||
|
processBuilder.redirectErrorStream(false);
|
||||||
|
var process = processBuilder.start();
|
||||||
|
|
||||||
|
state.tunnelProcess = process;
|
||||||
|
|
||||||
|
// Write config JSON to stdin, then keep stdin open for control messages
|
||||||
|
var config = buildConfig(state);
|
||||||
|
var stdinStream = process.getOutputStream();
|
||||||
|
stdinStream.write(config.getBytes(StandardCharsets.UTF_8));
|
||||||
|
stdinStream.write('\n');
|
||||||
|
stdinStream.flush();
|
||||||
|
|
||||||
|
// stdin is the control write channel
|
||||||
|
state.controlWriter = new PrintWriter(new OutputStreamWriter(stdinStream, StandardCharsets.UTF_8), true);
|
||||||
|
|
||||||
|
// Flush any pending control messages
|
||||||
|
for (var msg : state.pendingControlMessages) {
|
||||||
|
state.controlWriter.println(msg);
|
||||||
|
}
|
||||||
|
state.pendingControlMessages.clear();
|
||||||
|
|
||||||
|
// If accept was deferred, send it now
|
||||||
|
sendAcceptIfReady(state);
|
||||||
|
|
||||||
|
// Read control events from subprocess stdout
|
||||||
|
Thread.ofVirtual()
|
||||||
|
.name("control-read-" + callIdUnsigned(state.callId))
|
||||||
|
.start(() -> readControlEvents(state, process.getInputStream()));
|
||||||
|
|
||||||
|
// Drain subprocess stderr to prevent pipe buffer deadlock
|
||||||
|
Thread.ofVirtual().name("tunnel-stderr-" + callIdUnsigned(state.callId)).start(() -> {
|
||||||
|
try (var reader = new BufferedReader(new InputStreamReader(process.getErrorStream(),
|
||||||
|
StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
logger.debug("[tunnel-{}] {}", callIdUnsigned(state.callId), line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor process exit
|
||||||
|
process.onExit().thenAcceptAsync(p -> {
|
||||||
|
logger.debug("Tunnel for call {} exited with code {}", callIdUnsigned(state.callId), p.exitValue());
|
||||||
|
if (activeCalls.containsKey(state.callId)) {
|
||||||
|
endCall(state.callId, "tunnel_exit");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug("Spawned signal-call-tunnel for call {}", callIdUnsigned(state.callId));
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to spawn tunnel for call {}", callIdUnsigned(state.callId), e);
|
||||||
|
endCall(state.callId, "tunnel_spawn_error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findTunnelBinary() {
|
||||||
|
// Check environment variable first
|
||||||
|
var envPath = System.getenv("SIGNAL_CALL_TUNNEL_BIN");
|
||||||
|
if (envPath != null && !envPath.isEmpty()) {
|
||||||
|
return envPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check relative to the signal-cli installation directory
|
||||||
|
try {
|
||||||
|
var codeSource = CallManager.class.getProtectionDomain().getCodeSource();
|
||||||
|
if (codeSource != null) {
|
||||||
|
var jarPath = Path.of(codeSource.getLocation().toURI());
|
||||||
|
var binPath = tunnelBinaryFromCodeSourcePath(jarPath);
|
||||||
|
if (Files.isExecutable(binPath)) {
|
||||||
|
return binPath.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.debug("Failed to determine install dir from code source", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to PATH
|
||||||
|
return "signal-call-tunnel";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the expected tunnel binary path from a code source path.
|
||||||
|
* The code source (jar or class dir) is expected to be in {@code <install>/lib/},
|
||||||
|
* so we go up two levels to reach the install root, then look for
|
||||||
|
* {@code bin/signal-call-tunnel}.
|
||||||
|
*/
|
||||||
|
static Path tunnelBinaryFromCodeSourcePath(Path codeSourcePath) {
|
||||||
|
var installDir = codeSourcePath.getParent().getParent();
|
||||||
|
return installDir.resolve("bin").resolve("signal-call-tunnel");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildConfig(CallState state) {
|
||||||
|
var config = mapper.createObjectNode();
|
||||||
|
config.put("call_id", Utils.callIdUnsigned(state.callId));
|
||||||
|
config.put("is_outgoing", state.isOutgoing);
|
||||||
|
config.put("local_device_id", 1);
|
||||||
|
return writeJson(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readControlEvents(CallState state, java.io.InputStream inputStream) {
|
||||||
|
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
logger.debug("Control event for call {}: {}", callIdUnsigned(state.callId), line);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var json = mapper.readTree(line);
|
||||||
|
var type = json.has("type") ? json.get("type").asText() : "";
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "ready" -> {
|
||||||
|
if (json.has("inputDeviceName")) {
|
||||||
|
state.inputDeviceName = json.get("inputDeviceName").asText();
|
||||||
|
}
|
||||||
|
if (json.has("outputDeviceName")) {
|
||||||
|
state.outputDeviceName = json.get("outputDeviceName").asText();
|
||||||
|
}
|
||||||
|
logger.debug("Tunnel ready for call {}: input={}, output={}",
|
||||||
|
callIdUnsigned(state.callId),
|
||||||
|
state.inputDeviceName,
|
||||||
|
state.outputDeviceName);
|
||||||
|
}
|
||||||
|
case "sendOffer" -> {
|
||||||
|
var opaqueB64 = json.get("opaque").asText();
|
||||||
|
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
|
||||||
|
logSendMessageResult(sendOfferViaSignal(state, opaque));
|
||||||
|
}
|
||||||
|
case "sendAnswer" -> {
|
||||||
|
var opaqueB64 = json.get("opaque").asText();
|
||||||
|
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
|
||||||
|
logSendMessageResult(sendAnswerViaSignal(state, opaque));
|
||||||
|
}
|
||||||
|
case "sendIce" -> {
|
||||||
|
var candidatesArr = json.get("candidates");
|
||||||
|
var opaqueList = new ArrayList<byte[]>();
|
||||||
|
for (var c : candidatesArr) {
|
||||||
|
opaqueList.add(java.util.Base64.getDecoder().decode(c.get("opaque").asText()));
|
||||||
|
}
|
||||||
|
logSendMessageResult(sendIceViaSignal(state, opaqueList));
|
||||||
|
}
|
||||||
|
case "sendHangup" -> {
|
||||||
|
// RingRTC wants us to send a hangup message via Signal protocol.
|
||||||
|
// This is NOT a local state change — local state is handled by stateChange events.
|
||||||
|
var hangupType = json.has("hangupType")
|
||||||
|
? json.get("hangupType").asText("normal")
|
||||||
|
: "normal";
|
||||||
|
// Skip multi-device hangup types — signal-cli is single-device,
|
||||||
|
// and sending these to the remote peer causes it to terminate the call.
|
||||||
|
if (hangupType.contains("onanotherdevice")) {
|
||||||
|
logger.debug("Ignoring multi-device hangup type: {}", hangupType);
|
||||||
|
} else {
|
||||||
|
logSendMessageResult(sendHangupViaSignal(state, hangupType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "sendBusy" -> {
|
||||||
|
logSendMessageResult(sendBusyViaSignal(state));
|
||||||
|
}
|
||||||
|
case "stateChange" -> {
|
||||||
|
var ringrtcState = json.get("state").asText();
|
||||||
|
var reason = json.has("reason") ? json.get("reason").asText(null) : null;
|
||||||
|
handleStateChange(state, ringrtcState, reason);
|
||||||
|
}
|
||||||
|
case "error" -> {
|
||||||
|
var message = json.has("message") ? json.get("message").asText("unknown") : "unknown";
|
||||||
|
logger.error("Tunnel error for call {}: {}", callIdUnsigned(state.callId), message);
|
||||||
|
endCall(state.callId, "tunnel_error");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
logger.debug("Unknown control event type '{}' for call {}",
|
||||||
|
type,
|
||||||
|
callIdUnsigned(state.callId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to parse control event JSON for call {}: {}",
|
||||||
|
callIdUnsigned(state.callId),
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.debug("Control read ended for call {}: {}", callIdUnsigned(state.callId), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStateChange(CallState state, String ringrtcState, String reason) {
|
||||||
|
if (ringrtcState.startsWith("Incoming")) {
|
||||||
|
// Don't downgrade if we've already accepted
|
||||||
|
if (state.state == CallInfo.State.CONNECTING) return;
|
||||||
|
state.state = CallInfo.State.RINGING_INCOMING;
|
||||||
|
} else if (ringrtcState.startsWith("Outgoing")) {
|
||||||
|
state.state = CallInfo.State.RINGING_OUTGOING;
|
||||||
|
} else if ("Ringing".equals(ringrtcState)) {
|
||||||
|
// Tunnel is now ready to accept — flush deferred accept if pending
|
||||||
|
state.tunnelRinging = true;
|
||||||
|
sendAcceptIfReady(state);
|
||||||
|
return;
|
||||||
|
} else if ("Connected".equals(ringrtcState)) {
|
||||||
|
state.state = CallInfo.State.CONNECTED;
|
||||||
|
} else if ("Connecting".equals(ringrtcState)) {
|
||||||
|
state.state = CallInfo.State.RECONNECTING;
|
||||||
|
} else if ("Ended".equals(ringrtcState) || "Rejected".equals(ringrtcState)) {
|
||||||
|
endCall(state.callId, reason != null ? reason : ringrtcState.toLowerCase());
|
||||||
|
return;
|
||||||
|
} else if ("Concluded".equals(ringrtcState)) {
|
||||||
|
// Cleanup, no-op
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fireCallEvent(state, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void logSendMessageResult(SendMessageResult result) {
|
||||||
|
var identifier = result.getAddress().getIdentifier();
|
||||||
|
if (result.getProofRequiredFailure() != null) {
|
||||||
|
final var failure = result.getProofRequiredFailure();
|
||||||
|
logger.warn(
|
||||||
|
"CAPTCHA proof required for sending to \"{}\", available options \"{}\" with challenge token \"{}\", or wait \"{}\" seconds.\n",
|
||||||
|
identifier,
|
||||||
|
failure.getOptions()
|
||||||
|
.stream()
|
||||||
|
.map(ProofRequiredException.Option::toString)
|
||||||
|
.collect(Collectors.joining(", ")),
|
||||||
|
failure.getToken(),
|
||||||
|
failure.getRetryAfterSeconds());
|
||||||
|
} else if (result.isNetworkFailure()) {
|
||||||
|
logger.warn("Network failure for \"{}\"", identifier);
|
||||||
|
} else if (result.getRateLimitFailure() != null) {
|
||||||
|
logger.warn("Rate limit failure for \"{}\"", identifier);
|
||||||
|
} else if (result.isUnregisteredFailure()) {
|
||||||
|
logger.warn("Unregistered user \"{}\"", identifier);
|
||||||
|
} else if (result.getIdentityFailure() != null) {
|
||||||
|
logger.warn("Untrusted Identity for \"{}\"", identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAcceptIfReady(CallState state) {
|
||||||
|
if (state.acceptPending && state.tunnelRinging && state.controlWriter != null) {
|
||||||
|
state.acceptPending = false;
|
||||||
|
logger.debug("Sending deferred accept for call {}", callIdUnsigned(state.callId));
|
||||||
|
var acceptMsg = mapper.createObjectNode();
|
||||||
|
acceptMsg.put("type", "accept");
|
||||||
|
state.controlWriter.println(writeJson(acceptMsg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendOfferViaSignal(CallState state, byte[] opaque) {
|
||||||
|
var offerMessage = new OfferMessage(state.callId, OfferMessage.Type.AUDIO_CALL, opaque);
|
||||||
|
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, state.deviceId);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
logger.debug("Sent offer via Signal for call {}", callIdUnsigned(state.callId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendAnswerViaSignal(CallState state, byte[] opaque) {
|
||||||
|
var answerMessage = new AnswerMessage(state.callId, opaque);
|
||||||
|
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, state.deviceId);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
logger.debug("Sent answer via Signal for call {}", callIdUnsigned(state.callId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendIceViaSignal(CallState state, List<byte[]> opaqueList) {
|
||||||
|
var iceUpdates = opaqueList.stream().map(opaque -> new IceUpdateMessage(state.callId, opaque)).toList();
|
||||||
|
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, state.deviceId);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
logger.debug("Sent {} ICE candidates via Signal for call {}", opaqueList.size(), callIdUnsigned(state.callId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendBusyViaSignal(CallState state) {
|
||||||
|
var busyMessage = new BusyMessage(state.callId);
|
||||||
|
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, state.deviceId);
|
||||||
|
return context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendHangupViaSignal(CallState state, String hangupType) {
|
||||||
|
var type = switch (hangupType) {
|
||||||
|
case "accepted", "acceptedonanotherdevice" -> HangupMessage.Type.ACCEPTED;
|
||||||
|
case "declined", "declinedonanotherdevice" -> HangupMessage.Type.DECLINED;
|
||||||
|
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
|
||||||
|
default -> HangupMessage.Type.NORMAL;
|
||||||
|
};
|
||||||
|
var hangupMessage = new HangupMessage(state.callId, type, 0);
|
||||||
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getRemoteIdentityKey(CallState state) {
|
||||||
|
try {
|
||||||
|
var address = context.getRecipientHelper().resolveSignalServiceAddress(state.recipientId);
|
||||||
|
var serviceId = address.getServiceId();
|
||||||
|
var identityInfo = account.getIdentityKeyStore().getIdentityInfo(serviceId);
|
||||||
|
if (identityInfo != null) {
|
||||||
|
return getRawIdentityKeyBytes(identityInfo.getIdentityKey());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to get remote identity key for call {}", callIdUnsigned(state.callId), e);
|
||||||
|
}
|
||||||
|
logger.warn("Using local identity key as fallback for remote identity key");
|
||||||
|
return getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip the 0x05 DJB type prefix from a serialized identity key to get the
|
||||||
|
* raw 32-byte Curve25519 public key. Signal Android does this via
|
||||||
|
* WebRtcUtil.getPublicKeyBytes() before passing keys to RingRTC.
|
||||||
|
*/
|
||||||
|
private static byte[] getRawIdentityKeyBytes(IdentityKey identityKey) {
|
||||||
|
var serializedKey = identityKey.serialize();
|
||||||
|
return getRawIdentityKeyBytes(serializedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getRawIdentityKeyBytes(final byte[] serializedKey) {
|
||||||
|
if (serializedKey.length == 33 && serializedKey[0] == 0x05) {
|
||||||
|
return java.util.Arrays.copyOfRange(serializedKey, 1, serializedKey.length);
|
||||||
|
}
|
||||||
|
return serializedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String writeJson(ObjectNode node) {
|
||||||
|
try {
|
||||||
|
return mapper.writeValueAsString(node);
|
||||||
|
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
|
||||||
|
throw new RuntimeException("Failed to serialize JSON", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endCall(final long callId, final String reason) {
|
||||||
|
var state = activeCalls.remove(callId);
|
||||||
|
dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
|
||||||
|
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
|
||||||
|
if (state == null) return;
|
||||||
|
|
||||||
|
state.state = CallInfo.State.ENDED;
|
||||||
|
fireCallEvent(state, reason);
|
||||||
|
logger.debug("Call {} ended: {}", callIdUnsigned(callId), reason);
|
||||||
|
|
||||||
|
// Send Signal protocol hangup to remote peer (unless they initiated the end)
|
||||||
|
if (!"remote_hangup".equals(reason)
|
||||||
|
&& !"rejected".equals(reason)
|
||||||
|
&& !"remote_busy".equals(reason)
|
||||||
|
&& !"ringrtc_hangup".equals(reason)) {
|
||||||
|
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, 0);
|
||||||
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
logger.warn("Failed to send hangup to remote for call {}", callIdUnsigned(callId));
|
||||||
|
logSendMessageResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send hangup via control channel (stdin) before killing process
|
||||||
|
if (state.controlWriter != null) {
|
||||||
|
try {
|
||||||
|
var hangupMsg = mapper.createObjectNode();
|
||||||
|
hangupMsg.put("type", "hangup");
|
||||||
|
state.controlWriter.println(writeJson(hangupMsg));
|
||||||
|
state.controlWriter.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.debug("Failed to send hangup via control channel", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill tunnel process
|
||||||
|
if (state.tunnelProcess != null && state.tunnelProcess.isAlive()) {
|
||||||
|
state.tunnelProcess.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRingTimeout(final long callId) {
|
||||||
|
var state = activeCalls.get(callId);
|
||||||
|
if (state == null) return;
|
||||||
|
|
||||||
|
if (state.state == CallInfo.State.RINGING_INCOMING || state.state == CallInfo.State.RINGING_OUTGOING) {
|
||||||
|
logger.debug("Call {} ring timeout", callIdUnsigned(callId));
|
||||||
|
endCall(callId, "ring_timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long generateCallId() {
|
||||||
|
return new BigInteger(64, new SecureRandom()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
for (var callId : new ArrayList<>(activeCalls.keySet())) {
|
||||||
|
endCall(callId, "shutdown");
|
||||||
|
}
|
||||||
|
synchronized (callEventListeners) {
|
||||||
|
callEventListeners.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal call state tracking ---
|
||||||
|
|
||||||
|
static class CallState {
|
||||||
|
|
||||||
|
final long callId;
|
||||||
|
volatile CallInfo.State state;
|
||||||
|
final RecipientId recipientId;
|
||||||
|
volatile Integer deviceId;
|
||||||
|
final boolean isOutgoing;
|
||||||
|
volatile String inputDeviceName;
|
||||||
|
volatile String outputDeviceName;
|
||||||
|
volatile Process tunnelProcess;
|
||||||
|
volatile PrintWriter controlWriter;
|
||||||
|
// Control messages queued before the tunnel process starts
|
||||||
|
final List<String> pendingControlMessages = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
// Accept deferred until tunnel reports Ringing state
|
||||||
|
volatile boolean acceptPending = false;
|
||||||
|
// True once the tunnel has reported "Ringing" (ready to accept)
|
||||||
|
volatile boolean tunnelRinging = false;
|
||||||
|
|
||||||
|
CallState(
|
||||||
|
long callId,
|
||||||
|
CallInfo.State state,
|
||||||
|
RecipientId recipientId,
|
||||||
|
final Integer deviceId,
|
||||||
|
boolean isOutgoing
|
||||||
|
) {
|
||||||
|
this.callId = callId;
|
||||||
|
this.state = state;
|
||||||
|
this.recipientId = recipientId;
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.isOutgoing = isOutgoing;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallInfo toCallInfo(RecipientAddressResolver addressResolver) {
|
||||||
|
return new CallInfo(callId,
|
||||||
|
state,
|
||||||
|
addressResolver.resolveRecipientAddress(recipientId).toApiRecipientAddress(),
|
||||||
|
inputDeviceName,
|
||||||
|
outputDeviceName,
|
||||||
|
isOutgoing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -55,7 +55,7 @@ public class ContactHelper {
|
|||||||
final var version = contact == null
|
final var version = contact == null
|
||||||
? 1
|
? 1
|
||||||
: contact.messageExpirationTimeVersion() == Integer.MAX_VALUE
|
: contact.messageExpirationTimeVersion() == Integer.MAX_VALUE
|
||||||
? Integer.MAX_VALUE
|
? Integer.MAX_VALUE
|
||||||
: contact.messageExpirationTimeVersion() + 1;
|
: contact.messageExpirationTimeVersion() + 1;
|
||||||
account.getContactStore()
|
account.getContactStore()
|
||||||
.storeContact(recipientId,
|
.storeContact(recipientId,
|
||||||
@ -85,12 +85,18 @@ public class ContactHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setContactBlocked(RecipientId recipientId, boolean blocked) {
|
public void setContactBlocked(RecipientId recipientId, boolean blocked) {
|
||||||
|
setContactBlocked(recipientId, blocked, blocked ? System.currentTimeMillis() : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactBlocked(RecipientId recipientId, boolean blocked, long blockedAt) {
|
||||||
var contact = account.getContactStore().getContact(recipientId);
|
var contact = account.getContactStore().getContact(recipientId);
|
||||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
if (blocked) {
|
if (blocked) {
|
||||||
builder.withIsProfileSharingEnabled(false);
|
builder.withIsProfileSharingEnabled(false);
|
||||||
}
|
}
|
||||||
account.getContactStore().storeContact(recipientId, builder.withIsBlocked(blocked).build());
|
account.getContactStore()
|
||||||
|
.storeContact(recipientId,
|
||||||
|
builder.withIsBlocked(blocked).withBlockedAt(blocked ? blockedAt : 0).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ public class Context implements AutoCloseable {
|
|||||||
|
|
||||||
private AccountHelper accountHelper;
|
private AccountHelper accountHelper;
|
||||||
private AttachmentHelper attachmentHelper;
|
private AttachmentHelper attachmentHelper;
|
||||||
|
private CallManager callManager;
|
||||||
private ContactHelper contactHelper;
|
private ContactHelper contactHelper;
|
||||||
private GroupHelper groupHelper;
|
private GroupHelper groupHelper;
|
||||||
private GroupV2Helper groupV2Helper;
|
private GroupV2Helper groupV2Helper;
|
||||||
@ -92,6 +93,10 @@ public class Context implements AutoCloseable {
|
|||||||
return getOrCreate(() -> attachmentHelper, () -> attachmentHelper = new AttachmentHelper(this));
|
return getOrCreate(() -> attachmentHelper, () -> attachmentHelper = new AttachmentHelper(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallManager getCallManager() {
|
||||||
|
return getOrCreate(() -> callManager, () -> callManager = new CallManager(this));
|
||||||
|
}
|
||||||
|
|
||||||
public ContactHelper getContactHelper() {
|
public ContactHelper getContactHelper() {
|
||||||
return getOrCreate(() -> contactHelper, () -> contactHelper = new ContactHelper(account));
|
return getOrCreate(() -> contactHelper, () -> contactHelper = new ContactHelper(account));
|
||||||
}
|
}
|
||||||
@ -172,6 +177,9 @@ public class Context implements AutoCloseable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
if (callManager != null) {
|
||||||
|
callManager.close();
|
||||||
|
}
|
||||||
jobExecutor.close();
|
jobExecutor.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,15 +28,16 @@ import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|||||||
import org.asamk.signal.manager.util.AttachmentUtils;
|
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
|
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.storageservice.protos.groups.GroupChangeResponse;
|
import org.signal.storageservice.storage.protos.groups.GroupChangeResponse;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupChange;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupJoinInfo;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog;
|
||||||
@ -47,7 +48,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStre
|
|||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -123,14 +123,15 @@ public class GroupHelper {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
||||||
return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty(), uploadSpec));
|
return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty(), uploadSpec));
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupInfoV2 getOrMigrateGroup(
|
public GroupInfoV2 getOrMigrateGroup(
|
||||||
final GroupMasterKey groupMasterKey,
|
final GroupMasterKey groupMasterKey,
|
||||||
final int revision,
|
final int revision,
|
||||||
final byte[] signedGroupChange
|
final byte[] signedGroupChange,
|
||||||
|
final boolean ignoreAvatars
|
||||||
) {
|
) {
|
||||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ public class GroupHelper {
|
|||||||
if (group != null) {
|
if (group != null) {
|
||||||
storeProfileKeysFromMembers(group);
|
storeProfileKeysFromMembers(group);
|
||||||
final var avatar = group.avatar;
|
final var avatar = group.avatar;
|
||||||
if (!avatar.isEmpty()) {
|
if (!avatar.isEmpty() && !ignoreAvatars) {
|
||||||
downloadGroupAvatar(groupId, groupSecretParams, avatar);
|
downloadGroupAvatar(groupId, groupSecretParams, avatar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,9 +193,9 @@ public class GroupHelper {
|
|||||||
final GroupInfoV2 groupInfoV2,
|
final GroupInfoV2 groupInfoV2,
|
||||||
final GroupChangeResponse groupChangeResponse
|
final GroupChangeResponse groupChangeResponse
|
||||||
) {
|
) {
|
||||||
if (groupChangeResponse.groupSendEndorsementsResponse.size() > 0) {
|
if (groupChangeResponse.group_send_endorsements_response.size() > 0) {
|
||||||
try {
|
try {
|
||||||
final var groupSendEndorsementsResponse = new GroupSendEndorsementsResponse(groupChangeResponse.groupSendEndorsementsResponse.toByteArray());
|
final var groupSendEndorsementsResponse = new GroupSendEndorsementsResponse(groupChangeResponse.group_send_endorsements_response.toByteArray());
|
||||||
|
|
||||||
updateGroupEndorsements(groupInfoV2.getGroupId(),
|
updateGroupEndorsements(groupInfoV2.getGroupId(),
|
||||||
groupInfoV2.getMasterKey(),
|
groupInfoV2.getMasterKey(),
|
||||||
@ -298,7 +299,9 @@ public class GroupHelper {
|
|||||||
final GroupPermission editDetailsPermission,
|
final GroupPermission editDetailsPermission,
|
||||||
final String avatarFile,
|
final String avatarFile,
|
||||||
final Integer expirationTimer,
|
final Integer expirationTimer,
|
||||||
final Boolean isAnnouncementGroup
|
final Boolean isAnnouncementGroup,
|
||||||
|
final String labelEmoji,
|
||||||
|
final String labelString
|
||||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
||||||
var group = getGroupForUpdating(groupId);
|
var group = getGroupForUpdating(groupId);
|
||||||
final var avatarBytes = readAvatarBytes(avatarFile);
|
final var avatarBytes = readAvatarBytes(avatarFile);
|
||||||
@ -322,7 +325,9 @@ public class GroupHelper {
|
|||||||
editDetailsPermission,
|
editDetailsPermission,
|
||||||
avatarBytes,
|
avatarBytes,
|
||||||
expirationTimer,
|
expirationTimer,
|
||||||
isAnnouncementGroup);
|
isAnnouncementGroup,
|
||||||
|
labelEmoji,
|
||||||
|
labelString);
|
||||||
} catch (ConflictException e) {
|
} catch (ConflictException e) {
|
||||||
// Detected conflicting update, refreshing group and trying again
|
// Detected conflicting update, refreshing group and trying again
|
||||||
group = getGroup(groupId, true);
|
group = getGroup(groupId, true);
|
||||||
@ -341,7 +346,9 @@ public class GroupHelper {
|
|||||||
editDetailsPermission,
|
editDetailsPermission,
|
||||||
avatarBytes,
|
avatarBytes,
|
||||||
expirationTimer,
|
expirationTimer,
|
||||||
isAnnouncementGroup);
|
isAnnouncementGroup,
|
||||||
|
labelEmoji,
|
||||||
|
labelString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,6 +363,28 @@ public class GroupHelper {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SendGroupMessageResults terminateGroup(final GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
||||||
|
final var group = getGroupForUpdating(groupId);
|
||||||
|
if (!(group instanceof GroupInfoV2)) {
|
||||||
|
throw new IOException("Terminating a group is only supported for Signal group v2 groups.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SendGroupMessageResults results;
|
||||||
|
try {
|
||||||
|
results = terminateGroupV2((GroupInfoV2) group);
|
||||||
|
} catch (ConflictException e) {
|
||||||
|
// Detected conflicting update, refreshing group and trying again
|
||||||
|
results = terminateGroupV2((GroupInfoV2) getGroup(groupId, true));
|
||||||
|
}
|
||||||
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendGroupMessageResults terminateGroupV2(final GroupInfoV2 group) throws IOException {
|
||||||
|
final var groupGroupChangePair = context.getGroupV2Helper().terminateGroup(group);
|
||||||
|
return sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||||
|
}
|
||||||
|
|
||||||
public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException {
|
public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException {
|
||||||
var group = getGroupForUpdating(groupId);
|
var group = getGroupForUpdating(groupId);
|
||||||
|
|
||||||
@ -391,7 +420,8 @@ public class GroupHelper {
|
|||||||
.joinGroup(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword(), groupJoinInfo);
|
.joinGroup(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword(), groupJoinInfo);
|
||||||
final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
|
final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
|
||||||
groupJoinInfo.revision + 1,
|
groupJoinInfo.revision + 1,
|
||||||
changeResponse.groupChange == null ? null : changeResponse.groupChange.encode());
|
changeResponse.group_change == null ? null : changeResponse.group_change.encode(),
|
||||||
|
false);
|
||||||
|
|
||||||
if (group.getGroup() == null) {
|
if (group.getGroup() == null) {
|
||||||
// Only requested member, can't send update to group members
|
// Only requested member, can't send update to group members
|
||||||
@ -429,12 +459,21 @@ public class GroupHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
|
public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
|
||||||
|
setGroupBlocked(groupId, blocked, blocked ? System.currentTimeMillis() : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupBlocked(
|
||||||
|
final GroupId groupId,
|
||||||
|
final boolean blocked,
|
||||||
|
final long blockedAt
|
||||||
|
) throws GroupNotFoundException {
|
||||||
var group = getGroup(groupId);
|
var group = getGroup(groupId);
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
throw new GroupNotFoundException(groupId);
|
throw new GroupNotFoundException(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
group.setBlocked(blocked);
|
group.setBlocked(blocked);
|
||||||
|
group.setBlockedAt(blocked ? blockedAt : 0);
|
||||||
account.getGroupStore().updateGroup(group);
|
account.getGroupStore().updateGroup(group);
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
}
|
}
|
||||||
@ -550,16 +589,24 @@ public class GroupHelper {
|
|||||||
private void storeProfileKeysFromMembers(final DecryptedGroup group) {
|
private void storeProfileKeysFromMembers(final DecryptedGroup group) {
|
||||||
for (var member : group.members) {
|
for (var member : group.members) {
|
||||||
final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
|
final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
|
||||||
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
|
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
|
||||||
final var profileStore = account.getProfileStore();
|
}
|
||||||
if (profileStore.getProfileKey(recipientId) != null) {
|
for (var member : group.requestingMembers) {
|
||||||
// We already have a profile key, not updating it from a non-authoritative source
|
final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
|
||||||
continue;
|
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
|
||||||
}
|
}
|
||||||
try {
|
}
|
||||||
profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray()));
|
|
||||||
} catch (InvalidInputException ignored) {
|
private void storeProfileKeyIfMissing(final ServiceId serviceId, final byte[] profileKeyBytes) {
|
||||||
}
|
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
|
||||||
|
final var profileStore = account.getProfileStore();
|
||||||
|
if (profileStore.getProfileKey(recipientId) != null) {
|
||||||
|
// We already have a profile key, not updating it from a non-authoritative source
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
profileStore.storeProfileKey(recipientId, new ProfileKey(profileKeyBytes));
|
||||||
|
} catch (InvalidInputException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -680,7 +727,7 @@ public class GroupHelper {
|
|||||||
|
|
||||||
private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||||
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
|
||||||
context.getSendHelper().sendAsGroupMessage(messageBuilder, groupId, false, Optional.empty());
|
context.getSendHelper().sendAsGroupMessage(messageBuilder, groupId, false, Optional.empty(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SendGroupMessageResults updateGroupV2(
|
private SendGroupMessageResults updateGroupV2(
|
||||||
@ -699,7 +746,9 @@ public class GroupHelper {
|
|||||||
final GroupPermission editDetailsPermission,
|
final GroupPermission editDetailsPermission,
|
||||||
final byte[] avatarFile,
|
final byte[] avatarFile,
|
||||||
final Integer expirationTimer,
|
final Integer expirationTimer,
|
||||||
final Boolean isAnnouncementGroup
|
final Boolean isAnnouncementGroup,
|
||||||
|
final String labelEmoji,
|
||||||
|
final String labelString
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
SendGroupMessageResults result = null;
|
SendGroupMessageResults result = null;
|
||||||
final var groupV2Helper = context.getGroupV2Helper();
|
final var groupV2Helper = context.getGroupV2Helper();
|
||||||
@ -716,7 +765,7 @@ public class GroupHelper {
|
|||||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||||
}
|
}
|
||||||
final var newMembers = new HashSet<>(members);
|
final var newMembers = new HashSet<>(members);
|
||||||
newMembers.removeAll(group.getMembers());
|
newMembers.removeAll(group.getMemberRecipientIds());
|
||||||
newMembers.removeAll(group.getRequestingMembers());
|
newMembers.removeAll(group.getRequestingMembers());
|
||||||
if (!newMembers.isEmpty()) {
|
if (!newMembers.isEmpty()) {
|
||||||
var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
|
var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
|
||||||
@ -729,7 +778,7 @@ public class GroupHelper {
|
|||||||
if (banMembers != null) {
|
if (banMembers != null) {
|
||||||
existingRemoveMembers.addAll(banMembers);
|
existingRemoveMembers.addAll(banMembers);
|
||||||
}
|
}
|
||||||
existingRemoveMembers.retainAll(group.getMembers());
|
existingRemoveMembers.retainAll(group.getMemberRecipientIds());
|
||||||
if (members != null) {
|
if (members != null) {
|
||||||
existingRemoveMembers.removeAll(members);
|
existingRemoveMembers.removeAll(members);
|
||||||
}
|
}
|
||||||
@ -755,28 +804,20 @@ public class GroupHelper {
|
|||||||
|
|
||||||
if (admins != null) {
|
if (admins != null) {
|
||||||
final var newAdmins = new HashSet<>(admins);
|
final var newAdmins = new HashSet<>(admins);
|
||||||
newAdmins.retainAll(group.getMembers());
|
newAdmins.retainAll(group.getMemberRecipientIds());
|
||||||
newAdmins.removeAll(group.getAdminMembers());
|
newAdmins.removeAll(group.getAdminMemberRecipientIds());
|
||||||
if (!newAdmins.isEmpty()) {
|
if (!newAdmins.isEmpty()) {
|
||||||
for (var admin : newAdmins) {
|
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, newAdmins, true);
|
||||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
|
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||||
result = sendUpdateGroupV2Message(group,
|
|
||||||
groupGroupChangePair.first(),
|
|
||||||
groupGroupChangePair.second());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removeAdmins != null) {
|
if (removeAdmins != null) {
|
||||||
final var existingRemoveAdmins = new HashSet<>(removeAdmins);
|
final var existingRemoveAdmins = new HashSet<>(removeAdmins);
|
||||||
existingRemoveAdmins.retainAll(group.getAdminMembers());
|
existingRemoveAdmins.retainAll(group.getAdminMemberRecipientIds());
|
||||||
if (!existingRemoveAdmins.isEmpty()) {
|
if (!existingRemoveAdmins.isEmpty()) {
|
||||||
for (var admin : existingRemoveAdmins) {
|
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, existingRemoveAdmins, false);
|
||||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
|
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||||
result = sendUpdateGroupV2Message(group,
|
|
||||||
groupGroupChangePair.first(),
|
|
||||||
groupGroupChangePair.second());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -828,6 +869,15 @@ public class GroupHelper {
|
|||||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (labelString != null || labelEmoji != null) {
|
||||||
|
final var selfRecipientId = account.getSelfRecipientId();
|
||||||
|
final var selfMember = group.getMember(selfRecipientId);
|
||||||
|
var groupGroupChangePair = groupV2Helper.setMemberLabels(group,
|
||||||
|
labelEmoji != null ? labelEmoji : selfMember.labelEmoji(),
|
||||||
|
labelString != null ? labelString : selfMember.labelString());
|
||||||
|
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||||
|
}
|
||||||
|
|
||||||
if (name != null || description != null || avatarFile != null) {
|
if (name != null || description != null || avatarFile != null) {
|
||||||
var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
|
var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
|
||||||
if (avatarFile != null) {
|
if (avatarFile != null) {
|
||||||
@ -857,9 +907,9 @@ public class GroupHelper {
|
|||||||
final GroupInfoV2 groupInfoV2,
|
final GroupInfoV2 groupInfoV2,
|
||||||
final Set<RecipientId> newAdmins
|
final Set<RecipientId> newAdmins
|
||||||
) throws LastGroupAdminException, IOException {
|
) throws LastGroupAdminException, IOException {
|
||||||
final var currentAdmins = groupInfoV2.getAdminMembers();
|
final var currentAdmins = groupInfoV2.getAdminMemberRecipientIds();
|
||||||
newAdmins.removeAll(currentAdmins);
|
newAdmins.removeAll(currentAdmins);
|
||||||
newAdmins.retainAll(groupInfoV2.getMembers());
|
newAdmins.retainAll(groupInfoV2.getMemberRecipientIds());
|
||||||
if (currentAdmins.contains(account.getSelfRecipientId())
|
if (currentAdmins.contains(account.getSelfRecipientId())
|
||||||
&& currentAdmins.size() == 1
|
&& currentAdmins.size() == 1
|
||||||
&& groupInfoV2.getMembers().size() > 1
|
&& groupInfoV2.getMembers().size() > 1
|
||||||
@ -873,10 +923,10 @@ public class GroupHelper {
|
|||||||
|
|
||||||
final var groupChangeResponse = groupGroupChangePair.second();
|
final var groupChangeResponse = groupGroupChangePair.second();
|
||||||
handleGroupChangeResponse(groupInfoV2, groupChangeResponse);
|
handleGroupChangeResponse(groupInfoV2, groupChangeResponse);
|
||||||
if (groupChangeResponse.groupChange == null) {
|
if (groupChangeResponse.group_change == null) {
|
||||||
throw new AssertionError("groupChange is null");
|
throw new AssertionError("groupChange is null");
|
||||||
}
|
}
|
||||||
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupChangeResponse.groupChange.encode());
|
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupChangeResponse.group_change.encode());
|
||||||
return sendGroupMessage(messageBuilder,
|
return sendGroupMessage(messageBuilder,
|
||||||
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
|
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
|
||||||
groupInfoV2);
|
groupInfoV2);
|
||||||
@ -886,7 +936,7 @@ public class GroupHelper {
|
|||||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
|
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
|
||||||
.withId(g.getGroupId().serialize())
|
.withId(g.getGroupId().serialize())
|
||||||
.withName(g.name)
|
.withName(g.name)
|
||||||
.withMembers(g.getMembers()
|
.withMembers(g.getMemberRecipientIds()
|
||||||
.stream()
|
.stream()
|
||||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||||
.toList());
|
.toList());
|
||||||
@ -924,10 +974,10 @@ public class GroupHelper {
|
|||||||
members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
|
members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
|
||||||
account.getGroupStore().updateGroup(group);
|
account.getGroupStore().updateGroup(group);
|
||||||
|
|
||||||
if (groupChangeResponse.groupChange == null) {
|
if (groupChangeResponse.group_change == null) {
|
||||||
throw new AssertionError("groupChange is null");
|
throw new AssertionError("groupChange is null");
|
||||||
}
|
}
|
||||||
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChangeResponse.groupChange.encode());
|
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChangeResponse.group_change.encode());
|
||||||
return sendGroupMessage(messageBuilder, members, group);
|
return sendGroupMessage(messageBuilder, members, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,10 @@ import org.asamk.signal.manager.internal.SignalDependencies;
|
|||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||||
@ -17,15 +21,16 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
|||||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||||
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
|
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.storageservice.protos.groups.AccessControl;
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.signal.storageservice.protos.groups.GroupChange;
|
import org.signal.storageservice.storage.protos.groups.AccessControl;
|
||||||
import org.signal.storageservice.protos.groups.GroupChangeResponse;
|
import org.signal.storageservice.storage.protos.groups.GroupChange;
|
||||||
import org.signal.storageservice.protos.groups.Member;
|
import org.signal.storageservice.storage.protos.groups.GroupChangeResponse;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.storage.protos.groups.Member;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupChange;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupJoinInfo;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedMember;
|
||||||
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedPendingMember;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptChangeVerificationMode;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptChangeVerificationMode;
|
||||||
@ -38,12 +43,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString
|
|||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
|
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -225,7 +225,7 @@ class GroupV2Helper {
|
|||||||
change.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction.Builder().avatar(avatarCdnKey).build());
|
change.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction.Builder().avatar(avatarCdnKey).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
change.sourceServiceId(getSelfAci().toByteString());
|
change.sourceUserId(getSelfAci().toByteString());
|
||||||
|
|
||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
@ -252,7 +252,7 @@ class GroupV2Helper {
|
|||||||
final var aci = getSelfAci();
|
final var aci = getSelfAci();
|
||||||
final var change = groupOperations.createModifyGroupMembershipChange(candidates, bannedUuids, aci);
|
final var change = groupOperations.createModifyGroupMembershipChange(candidates, bannedUuids, aci);
|
||||||
|
|
||||||
change.sourceServiceId(getSelfAci().toByteString());
|
change.sourceUserId(getSelfAci().toByteString());
|
||||||
|
|
||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
@ -264,6 +264,9 @@ class GroupV2Helper {
|
|||||||
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
|
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
|
||||||
final var selfAci = getSelfAci();
|
final var selfAci = getSelfAci();
|
||||||
var selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, selfAci);
|
var selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, selfAci);
|
||||||
|
if (selfPendingMember.isEmpty()) {
|
||||||
|
selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, getSelfPni());
|
||||||
|
}
|
||||||
|
|
||||||
if (selfPendingMember.isPresent()) {
|
if (selfPendingMember.isPresent()) {
|
||||||
return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
|
return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
|
||||||
@ -343,7 +346,7 @@ class GroupV2Helper {
|
|||||||
false,
|
false,
|
||||||
groupInfoV2.getGroup().bannedMembers);
|
groupInfoV2.getGroup().bannedMembers);
|
||||||
|
|
||||||
change.sourceServiceId(getSelfAci().toByteString());
|
change.sourceUserId(getSelfAci().toByteString());
|
||||||
|
|
||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
@ -360,7 +363,7 @@ class GroupV2Helper {
|
|||||||
|
|
||||||
final var change = groupOperations.createUnbanServiceIdsChange(serviceIds);
|
final var change = groupOperations.createUnbanServiceIdsChange(serviceIds);
|
||||||
|
|
||||||
change.sourceServiceId(getSelfAci().toByteString());
|
change.sourceUserId(getSelfAci().toByteString());
|
||||||
|
|
||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
@ -436,7 +439,7 @@ class GroupV2Helper {
|
|||||||
|
|
||||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||||
final var change = groupOperations.createUpdateProfileKeyCredentialChange(profileKeyCredential);
|
final var change = groupOperations.createUpdateProfileKeyCredentialChange(profileKeyCredential);
|
||||||
change.sourceServiceId(getSelfAci().toByteString());
|
change.sourceUserId(getSelfAci().toByteString());
|
||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,7 +462,7 @@ class GroupV2Helper {
|
|||||||
? groupOperations.createGroupJoinRequest(profileKeyCredential)
|
? groupOperations.createGroupJoinRequest(profileKeyCredential)
|
||||||
: groupOperations.createGroupJoinDirect(profileKeyCredential);
|
: groupOperations.createGroupJoinDirect(profileKeyCredential);
|
||||||
|
|
||||||
change.sourceServiceId(context.getRecipientHelper()
|
change.sourceUserId(context.getRecipientHelper()
|
||||||
.resolveSignalServiceAddress(selfRecipientId)
|
.resolveSignalServiceAddress(selfRecipientId)
|
||||||
.getServiceId()
|
.getServiceId()
|
||||||
.toByteString());
|
.toByteString());
|
||||||
@ -476,28 +479,50 @@ class GroupV2Helper {
|
|||||||
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
|
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
|
// We need to accept the invite with the ACI or PNI that was used for the invitation
|
||||||
|
final var selfAddress = context.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId);
|
||||||
|
final var selfServiceId = groupInfoV2.getPendingMemberAddresses()
|
||||||
|
.stream()
|
||||||
|
.filter(s -> s.matches(selfAddress))
|
||||||
|
.findFirst();
|
||||||
|
if (selfServiceId.isEmpty()) {
|
||||||
|
throw new IOException("Cannot find service ID for self to accept invite");
|
||||||
|
}
|
||||||
|
final var serviceId = selfServiceId.get().getServiceId();
|
||||||
|
|
||||||
final var aci = context.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId).getServiceId();
|
final GroupChange.Actions.Builder change;
|
||||||
change.sourceServiceId(aci.toByteString());
|
if (serviceId instanceof ACI) {
|
||||||
|
change = groupOperations.createAcceptInviteChange(profileKeyCredential);
|
||||||
|
} else {
|
||||||
|
change = groupOperations.createAcceptPniInviteChange(profileKeyCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
change.sourceUserId(serviceId.toByteString());
|
||||||
|
|
||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
|
Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
|
||||||
GroupInfoV2 groupInfoV2,
|
GroupInfoV2 groupInfoV2,
|
||||||
RecipientId recipientId,
|
Set<RecipientId> recipientIds,
|
||||||
boolean admin
|
boolean admin
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||||
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
|
||||||
final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT;
|
final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT;
|
||||||
if (address.getServiceId() instanceof ACI aci) {
|
final var change = new GroupChange.Actions.Builder();
|
||||||
final var change = groupOperations.createChangeMemberRole(aci, newRole);
|
final var memberRoles = recipientIds.stream()
|
||||||
return commitChange(groupInfoV2, change);
|
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||||
} else {
|
.map(SignalServiceAddress::getServiceId)
|
||||||
|
.filter(m -> m instanceof ACI)
|
||||||
|
.map(m -> (ACI) m)
|
||||||
|
.map(aci -> new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(groupOperations.encryptServiceId(
|
||||||
|
aci)).role(newRole).build())
|
||||||
|
.toList();
|
||||||
|
if (memberRoles.size() < recipientIds.size()) {
|
||||||
throw new IllegalArgumentException("Can't make a PNI a group admin.");
|
throw new IllegalArgumentException("Can't make a PNI a group admin.");
|
||||||
}
|
}
|
||||||
|
change.modifyMemberRoles(memberRoles);
|
||||||
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pair<DecryptedGroup, GroupChangeResponse> setMessageExpirationTimer(
|
Pair<DecryptedGroup, GroupChangeResponse> setMessageExpirationTimer(
|
||||||
@ -518,6 +543,26 @@ class GroupV2Helper {
|
|||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pair<DecryptedGroup, GroupChangeResponse> terminateGroup(
|
||||||
|
GroupInfoV2 groupInfoV2
|
||||||
|
) throws IOException {
|
||||||
|
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||||
|
final var change = groupOperations.createTerminateGroup();
|
||||||
|
return commitChange(groupInfoV2, change);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pair<DecryptedGroup, GroupChangeResponse> setMemberLabels(
|
||||||
|
GroupInfoV2 groupInfoV2,
|
||||||
|
String labelEmoji,
|
||||||
|
String labelString
|
||||||
|
) throws IOException {
|
||||||
|
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||||
|
final var change = groupOperations.createChangeMemberLabel(getSelfAci(),
|
||||||
|
labelString == null ? "" : labelString,
|
||||||
|
labelEmoji);
|
||||||
|
return commitChange(groupInfoV2, change);
|
||||||
|
}
|
||||||
|
|
||||||
private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
|
private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
|
||||||
return switch (state) {
|
return switch (state) {
|
||||||
case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE;
|
case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE;
|
||||||
@ -585,7 +630,7 @@ class GroupV2Helper {
|
|||||||
final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
|
final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
|
||||||
final var previousGroupState = groupInfoV2.getGroup();
|
final var previousGroupState = groupInfoV2.getGroup();
|
||||||
final var nextRevision = previousGroupState.revision + 1;
|
final var nextRevision = previousGroupState.revision + 1;
|
||||||
final var changeActions = change.revision(nextRevision).build();
|
final var changeActions = change.version(nextRevision).build();
|
||||||
final DecryptedGroupChange decryptedChange;
|
final DecryptedGroupChange decryptedChange;
|
||||||
final DecryptedGroup decryptedGroupState;
|
final DecryptedGroup decryptedGroupState;
|
||||||
|
|
||||||
@ -611,7 +656,7 @@ class GroupV2Helper {
|
|||||||
GroupLinkPassword password
|
GroupLinkPassword password
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var nextRevision = currentRevision + 1;
|
final var nextRevision = currentRevision + 1;
|
||||||
final var changeActions = change.revision(nextRevision).build();
|
final var changeActions = change.version(nextRevision).build();
|
||||||
|
|
||||||
return dependencies.getGroupsV2Api()
|
return dependencies.getGroupsV2Api()
|
||||||
.patchGroup(changeActions,
|
.patchGroup(changeActions,
|
||||||
@ -621,6 +666,9 @@ class GroupV2Helper {
|
|||||||
|
|
||||||
Pair<ServiceId, ProfileKey> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change) {
|
Pair<ServiceId, ProfileKey> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change) {
|
||||||
UUID editor = UuidUtil.fromByteStringOrNull(change.editorServiceIdBytes);
|
UUID editor = UuidUtil.fromByteStringOrNull(change.editorServiceIdBytes);
|
||||||
|
if (editor == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
final var editorProfileKeyBytes = Stream.concat(Stream.of(change.newMembers.stream(),
|
final var editorProfileKeyBytes = Stream.concat(Stream.of(change.newMembers.stream(),
|
||||||
change.promotePendingMembers.stream(),
|
change.promotePendingMembers.stream(),
|
||||||
change.modifiedProfileKeys.stream())
|
change.modifiedProfileKeys.stream())
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.TrustLevel;
|
|||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
|
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
|
||||||
import org.signal.libsignal.protocol.fingerprint.FingerprintParsingException;
|
import org.signal.libsignal.protocol.fingerprint.FingerprintParsingException;
|
||||||
@ -12,7 +13,6 @@ import org.signal.libsignal.protocol.fingerprint.ScannableFingerprint;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import org.asamk.signal.manager.actions.HandleAction;
|
|||||||
import org.asamk.signal.manager.actions.RefreshPreKeysAction;
|
import org.asamk.signal.manager.actions.RefreshPreKeysAction;
|
||||||
import org.asamk.signal.manager.actions.RenewSessionAction;
|
import org.asamk.signal.manager.actions.RenewSessionAction;
|
||||||
import org.asamk.signal.manager.actions.ResendMessageAction;
|
import org.asamk.signal.manager.actions.ResendMessageAction;
|
||||||
|
import org.asamk.signal.manager.actions.RetrieveDeviceNameAction;
|
||||||
import org.asamk.signal.manager.actions.RetrieveProfileAction;
|
import org.asamk.signal.manager.actions.RetrieveProfileAction;
|
||||||
import org.asamk.signal.manager.actions.SendGroupInfoAction;
|
import org.asamk.signal.manager.actions.SendGroupInfoAction;
|
||||||
import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
|
import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
|
||||||
@ -34,6 +35,9 @@ import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
|||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
||||||
|
import org.asamk.signal.manager.util.MimeUtils;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
|
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
|
||||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
|
import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
|
||||||
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||||
@ -41,7 +45,6 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
|||||||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||||
import org.signal.libsignal.metadata.SelfSendException;
|
import org.signal.libsignal.metadata.SelfSendException;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.signal.libsignal.protocol.UsePqRatchet;
|
|
||||||
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
||||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
@ -61,17 +64,21 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
|||||||
import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.internal.push.Envelope;
|
import org.whispersystems.signalservice.internal.push.Envelope;
|
||||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -102,11 +109,11 @@ public final class IncomingMessageHandler {
|
|||||||
SignalServiceContent content = null;
|
SignalServiceContent content = null;
|
||||||
if (!envelope.isReceipt()) {
|
if (!envelope.isReceipt()) {
|
||||||
account.getIdentityKeyStore().setRetryingDecryption(true);
|
account.getIdentityKeyStore().setRetryingDecryption(true);
|
||||||
final var destination = getDestination(envelope).serviceId();
|
|
||||||
try {
|
try {
|
||||||
|
final var destination = getDestination(envelope).serviceId();
|
||||||
final var cipherResult = dependencies.getCipher(destination == null
|
final var cipherResult = dependencies.getCipher(destination == null
|
||||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
|
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||||
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return new Pair<>(List.of(), null);
|
return new Pair<>(List.of(), null);
|
||||||
@ -133,18 +140,33 @@ public final class IncomingMessageHandler {
|
|||||||
final Manager.ReceiveMessageHandler handler
|
final Manager.ReceiveMessageHandler handler
|
||||||
) {
|
) {
|
||||||
final var actions = new ArrayList<HandleAction>();
|
final var actions = new ArrayList<HandleAction>();
|
||||||
|
if (envelope.isPreKeySignalMessage()) {
|
||||||
|
actions.add(RefreshPreKeysAction.create());
|
||||||
|
}
|
||||||
SignalServiceContent content = null;
|
SignalServiceContent content = null;
|
||||||
Exception exception = null;
|
Exception exception = null;
|
||||||
envelope.getSourceServiceId().map(ServiceId::parseOrNull)
|
if (envelope.getSourceServiceId() != null) {
|
||||||
// Store uuid if we don't have it already
|
// Store uuid if we don't have it already
|
||||||
// uuid in envelope is sent by server
|
// uuid in envelope is sent by server
|
||||||
.ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId));
|
account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId());
|
||||||
|
}
|
||||||
if (!envelope.isReceipt()) {
|
if (!envelope.isReceipt()) {
|
||||||
final var destination = getDestination(envelope).serviceId();
|
|
||||||
try {
|
try {
|
||||||
|
final var destination = getDestination(envelope).serviceId();
|
||||||
|
|
||||||
|
if (destination == account.getPni() && envelope.getSourceServiceId() == null) {
|
||||||
|
throw new InvalidMessageException(
|
||||||
|
"Got a sealed sender message to our PNI? Invalid message, ignoring.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envelope.getSourceServiceId() instanceof ServiceId.PNI
|
||||||
|
&& envelope.getProto().type != Envelope.Type.SERVER_DELIVERY_RECEIPT) {
|
||||||
|
throw new InvalidMessageException("Got a message from a PNI that was not a SERVER_DELIVERY_RECEIPT.");
|
||||||
|
}
|
||||||
|
|
||||||
final var cipherResult = dependencies.getCipher(destination == null
|
final var cipherResult = dependencies.getCipher(destination == null
|
||||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
|
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||||
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return new Pair<>(List.of(), null);
|
return new Pair<>(List.of(), null);
|
||||||
@ -166,7 +188,13 @@ public final class IncomingMessageHandler {
|
|||||||
logger.debug("Received invalid message from blocked contact, ignoring.");
|
logger.debug("Received invalid message from blocked contact, ignoring.");
|
||||||
} else {
|
} else {
|
||||||
var serviceId = ServiceId.parseOrNull(e.getSender());
|
var serviceId = ServiceId.parseOrNull(e.getSender());
|
||||||
if (serviceId != null) {
|
ServiceId destination;
|
||||||
|
try {
|
||||||
|
destination = getDestination(envelope).serviceId();
|
||||||
|
} catch (InvalidMessageException ex) {
|
||||||
|
destination = null;
|
||||||
|
}
|
||||||
|
if (serviceId != null && destination != null) {
|
||||||
final var isSelf = sender.equals(account.getSelfRecipientId())
|
final var isSelf = sender.equals(account.getSelfRecipientId())
|
||||||
&& e.getSenderDevice() == account.getDeviceId();
|
&& e.getSenderDevice() == account.getDeviceId();
|
||||||
logger.debug("Received invalid message, queuing renew session action.");
|
logger.debug("Received invalid message, queuing renew session action.");
|
||||||
@ -200,7 +228,10 @@ public final class IncomingMessageHandler {
|
|||||||
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException {
|
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException {
|
||||||
final var content = cipherResult.getContent();
|
final var content = cipherResult.getContent();
|
||||||
final var envelopeMetadata = cipherResult.getMetadata();
|
final var envelopeMetadata = cipherResult.getMetadata();
|
||||||
final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope, content, account.getAci());
|
final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope,
|
||||||
|
content,
|
||||||
|
account.getAci(),
|
||||||
|
cipherResult.getMetadata().getCiphertextMessageType());
|
||||||
|
|
||||||
if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) {
|
if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) {
|
||||||
logger.warn("Invalid content! {}", v.getReason(), v.getThrowable());
|
logger.warn("Invalid content! {}", v.getReason(), v.getThrowable());
|
||||||
@ -254,7 +285,7 @@ public final class IncomingMessageHandler {
|
|||||||
var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content);
|
var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content);
|
||||||
final var groupContext = getGroupContext(content);
|
final var groupContext = getGroupContext(content);
|
||||||
if (groupContext != null && groupContext.getGroupV2().isPresent()) {
|
if (groupContext != null && groupContext.getGroupV2().isPresent()) {
|
||||||
handleGroupV2Context(groupContext.getGroupV2().get());
|
handleGroupV2Context(groupContext.getGroupV2().get(), receiveConfig.ignoreAvatars());
|
||||||
}
|
}
|
||||||
// Check again in case the user just joined the group
|
// Check again in case the user just joined the group
|
||||||
notAllowedToSendToGroup = notAllowedToSendToGroup && isNotAllowedToSendToGroup(envelope, content);
|
notAllowedToSendToGroup = notAllowedToSendToGroup && isNotAllowedToSendToGroup(envelope, content);
|
||||||
@ -270,13 +301,18 @@ public final class IncomingMessageHandler {
|
|||||||
return List.of();
|
return List.of();
|
||||||
} else {
|
} else {
|
||||||
List<HandleAction> actions;
|
List<HandleAction> actions;
|
||||||
|
Map<String, String> longTexts;
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
actions = handleMessage(envelope, content, receiveConfig);
|
final var results = handleMessage(envelope, content, receiveConfig);
|
||||||
|
actions = results.first();
|
||||||
|
longTexts = results.second();
|
||||||
} else {
|
} else {
|
||||||
actions = List.of();
|
actions = List.of();
|
||||||
|
longTexts = Map.of();
|
||||||
}
|
}
|
||||||
handler.handleMessage(MessageEnvelope.from(envelope,
|
handler.handleMessage(MessageEnvelope.from(envelope,
|
||||||
content,
|
content,
|
||||||
|
longTexts,
|
||||||
account.getRecipientResolver(),
|
account.getRecipientResolver(),
|
||||||
account.getRecipientAddressResolver(),
|
account.getRecipientAddressResolver(),
|
||||||
context.getAttachmentHelper()::getAttachmentFile,
|
context.getAttachmentHelper()::getAttachmentFile,
|
||||||
@ -285,22 +321,28 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<HandleAction> handleMessage(
|
public Pair<List<HandleAction>, Map<String, String>> handleMessage(
|
||||||
SignalServiceEnvelope envelope,
|
SignalServiceEnvelope envelope,
|
||||||
SignalServiceContent content,
|
SignalServiceContent content,
|
||||||
ReceiveConfig receiveConfig
|
ReceiveConfig receiveConfig
|
||||||
) {
|
) {
|
||||||
var actions = new ArrayList<HandleAction>();
|
final var actions = new ArrayList<HandleAction>();
|
||||||
|
final var longTexts = new HashMap<String, String>();
|
||||||
final var senderDeviceAddress = getSender(envelope, content);
|
final var senderDeviceAddress = getSender(envelope, content);
|
||||||
final var sender = senderDeviceAddress.recipientId();
|
final var sender = senderDeviceAddress.recipientId();
|
||||||
final var senderServiceId = senderDeviceAddress.serviceId();
|
final var senderServiceId = senderDeviceAddress.serviceId();
|
||||||
final var senderDeviceId = senderDeviceAddress.deviceId();
|
final var senderDeviceId = senderDeviceAddress.deviceId();
|
||||||
final var destination = getDestination(envelope);
|
final DeviceAddress destination;
|
||||||
|
try {
|
||||||
|
destination = getDestination(envelope);
|
||||||
|
} catch (InvalidMessageException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (account.getPni().equals(destination.serviceId)) {
|
if (account.getPni().equals(destination.serviceId)) {
|
||||||
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true);
|
account.getRecipientStore().markNeedsPniSignature(sender, true);
|
||||||
} else if (account.getAci().equals(destination.serviceId)) {
|
} else if (account.getAci().equals(destination.serviceId)) {
|
||||||
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, false);
|
account.getRecipientStore().markNeedsPniSignature(sender, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.getReceiptMessage().isPresent()) {
|
if (content.getReceiptMessage().isPresent()) {
|
||||||
@ -365,27 +407,73 @@ public final class IncomingMessageHandler {
|
|||||||
message.getTimestamp()));
|
message.getTimestamp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.addAll(handleSignalServiceDataMessage(message,
|
final var dataResults = handleSignalServiceDataMessage(message,
|
||||||
false,
|
false,
|
||||||
senderDeviceAddress,
|
senderDeviceAddress,
|
||||||
destination,
|
destination,
|
||||||
receiveConfig.ignoreAttachments()));
|
receiveConfig);
|
||||||
|
actions.addAll(dataResults.first());
|
||||||
|
longTexts.putAll(dataResults.second());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.getStoryMessage().isPresent()) {
|
if (content.getStoryMessage().isPresent()) {
|
||||||
final var message = content.getStoryMessage().get();
|
final var message = content.getStoryMessage().get();
|
||||||
actions.addAll(handleSignalServiceStoryMessage(message, sender, receiveConfig.ignoreAttachments()));
|
actions.addAll(handleSignalServiceStoryMessage(message, sender, receiveConfig));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.getSyncMessage().isPresent()) {
|
if (content.getSyncMessage().isPresent()) {
|
||||||
var syncMessage = content.getSyncMessage().get();
|
var syncMessage = content.getSyncMessage().get();
|
||||||
actions.addAll(handleSyncMessage(envelope,
|
final var syncResults = handleSyncMessage(envelope, syncMessage, senderDeviceAddress, receiveConfig);
|
||||||
syncMessage,
|
actions.addAll(syncResults.first());
|
||||||
senderDeviceAddress,
|
longTexts.putAll(syncResults.second());
|
||||||
receiveConfig.ignoreAttachments()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return actions;
|
if (content.getCallMessage().isPresent()) {
|
||||||
|
handleCallMessage(content.getCallMessage().get(), sender, senderDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Pair<>(actions, longTexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCallMessage(
|
||||||
|
final SignalServiceCallMessage callMessage,
|
||||||
|
final RecipientId sender,
|
||||||
|
final int deviceId
|
||||||
|
) {
|
||||||
|
var callManager = context.getCallManager();
|
||||||
|
if (callMessage.getDestinationDeviceId().isPresent()
|
||||||
|
&& callMessage.getDestinationDeviceId().get() != account.getDeviceId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callMessage.getOfferMessage().ifPresent(offer -> {
|
||||||
|
var type = offer.getType()
|
||||||
|
== org.whispersystems.signalservice.api.messages.calls.OfferMessage.Type.VIDEO_CALL
|
||||||
|
? org.asamk.signal.manager.api.MessageEnvelope.Call.Offer.Type.VIDEO_CALL
|
||||||
|
: org.asamk.signal.manager.api.MessageEnvelope.Call.Offer.Type.AUDIO_CALL;
|
||||||
|
callManager.handleIncomingOffer(sender, deviceId, offer.getId(), type, offer.getOpaque());
|
||||||
|
});
|
||||||
|
|
||||||
|
callMessage.getAnswerMessage()
|
||||||
|
.ifPresent(answer -> callManager.handleIncomingAnswer(answer.getId(), deviceId, answer.getOpaque()));
|
||||||
|
|
||||||
|
callMessage.getIceUpdateMessages().ifPresent(iceUpdates -> {
|
||||||
|
for (var ice : iceUpdates) {
|
||||||
|
callManager.handleIncomingIceCandidate(ice.getId(), ice.getOpaque(), deviceId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callMessage.getHangupMessage().ifPresent(hangup -> {
|
||||||
|
// Only NORMAL hangups actually end the call. ACCEPTED/DECLINED/BUSY
|
||||||
|
// are multi-device notifications irrelevant for single-device signal-cli.
|
||||||
|
var hangupType = hangup.getType();
|
||||||
|
if (hangupType == org.whispersystems.signalservice.api.messages.calls.HangupMessage.Type.NORMAL
|
||||||
|
|| hangupType == null) {
|
||||||
|
callManager.handleIncomingHangup(hangup.getId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callMessage.getBusyMessage().ifPresent(busy -> callManager.handleIncomingBusy(busy.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean handlePniSignatureMessage(
|
private boolean handlePniSignatureMessage(
|
||||||
@ -410,10 +498,11 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
|
logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
|
||||||
account.getRecipientTrustedResolver()
|
final var recipientId = account.getRecipientTrustedResolver()
|
||||||
.resolveRecipientTrusted(Optional.of(ACI.from(aci.getRawUuid())),
|
.resolveRecipientTrusted(Optional.of(ACI.from(aci.getRawUuid())),
|
||||||
Optional.of(pni),
|
Optional.of(pni),
|
||||||
senderAddress.getNumber());
|
senderAddress.getNumber());
|
||||||
|
account.getRecipientStore().markPniSignatureVerified(recipientId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,19 +560,20 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HandleAction> handleSyncMessage(
|
private Pair<List<HandleAction>, Map<String, String>> handleSyncMessage(
|
||||||
final SignalServiceEnvelope envelope,
|
final SignalServiceEnvelope envelope,
|
||||||
final SignalServiceSyncMessage syncMessage,
|
final SignalServiceSyncMessage syncMessage,
|
||||||
final DeviceAddress sender,
|
final DeviceAddress sender,
|
||||||
final boolean ignoreAttachments
|
final ReceiveConfig receiveConfig
|
||||||
) {
|
) {
|
||||||
var actions = new ArrayList<HandleAction>();
|
final var actions = new ArrayList<HandleAction>();
|
||||||
|
final var longTexts = new HashMap<String, String>();
|
||||||
account.setMultiDevice(true);
|
account.setMultiDevice(true);
|
||||||
if (syncMessage.getSent().isPresent()) {
|
if (syncMessage.getSent().isPresent()) {
|
||||||
var message = syncMessage.getSent().get();
|
var message = syncMessage.getSent().get();
|
||||||
final var destination = message.getDestination().orElse(null);
|
final var destination = message.getDestination().orElse(null);
|
||||||
if (message.getDataMessage().isPresent()) {
|
if (message.getDataMessage().isPresent()) {
|
||||||
actions.addAll(handleSignalServiceDataMessage(message.getDataMessage().get(),
|
final var dataResults = handleSignalServiceDataMessage(message.getDataMessage().get(),
|
||||||
true,
|
true,
|
||||||
sender,
|
sender,
|
||||||
destination == null
|
destination == null
|
||||||
@ -491,12 +581,14 @@ public final class IncomingMessageHandler {
|
|||||||
: new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
|
: new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
|
||||||
destination.getServiceId(),
|
destination.getServiceId(),
|
||||||
0),
|
0),
|
||||||
ignoreAttachments));
|
receiveConfig);
|
||||||
|
actions.addAll(dataResults.first());
|
||||||
|
longTexts.putAll(dataResults.second());
|
||||||
}
|
}
|
||||||
if (message.getStoryMessage().isPresent()) {
|
if (message.getStoryMessage().isPresent()) {
|
||||||
actions.addAll(handleSignalServiceStoryMessage(message.getStoryMessage().get(),
|
actions.addAll(handleSignalServiceStoryMessage(message.getStoryMessage().get(),
|
||||||
sender.recipientId(),
|
sender.recipientId(),
|
||||||
ignoreAttachments));
|
receiveConfig));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (syncMessage.getRequest().isPresent() && account.isPrimaryDevice()) {
|
if (syncMessage.getRequest().isPresent() && account.isPrimaryDevice()) {
|
||||||
@ -522,7 +614,9 @@ public final class IncomingMessageHandler {
|
|||||||
try {
|
try {
|
||||||
final var groupsMessage = syncMessage.getGroups().get();
|
final var groupsMessage = syncMessage.getGroups().get();
|
||||||
context.getAttachmentHelper()
|
context.getAttachmentHelper()
|
||||||
.retrieveAttachment(groupsMessage, context.getSyncHelper()::handleSyncDeviceGroups);
|
.retrieveAttachment(groupsMessage,
|
||||||
|
input -> context.getSyncHelper()
|
||||||
|
.handleSyncDeviceGroups(input, receiveConfig.ignoreAvatars()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage());
|
logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
@ -532,13 +626,12 @@ public final class IncomingMessageHandler {
|
|||||||
for (var individual : blockedListMessage.individuals) {
|
for (var individual : blockedListMessage.individuals) {
|
||||||
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
||||||
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
||||||
context.getContactHelper().setContactBlocked(recipientId, true);
|
context.getContactHelper().setContactBlocked(recipientId, true, individual.getBlockedAt());
|
||||||
}
|
}
|
||||||
for (var groupId : blockedListMessage.groupIds.stream()
|
for (var group : blockedListMessage.groups) {
|
||||||
.map(GroupId::unknownVersion)
|
final var groupId = GroupId.unknownVersion(group.getGroupId());
|
||||||
.collect(Collectors.toSet())) {
|
|
||||||
try {
|
try {
|
||||||
context.getGroupHelper().setGroupBlocked(groupId, true);
|
context.getGroupHelper().setGroupBlocked(groupId, true, group.getBlockedAt());
|
||||||
} catch (GroupNotFoundException e) {
|
} catch (GroupNotFoundException e) {
|
||||||
logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
|
logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
|
||||||
groupId.toBase64());
|
groupId.toBase64());
|
||||||
@ -550,7 +643,8 @@ public final class IncomingMessageHandler {
|
|||||||
final var contactsMessage = syncMessage.getContacts().get();
|
final var contactsMessage = syncMessage.getContacts().get();
|
||||||
context.getAttachmentHelper()
|
context.getAttachmentHelper()
|
||||||
.retrieveAttachment(contactsMessage.getContactsStream(),
|
.retrieveAttachment(contactsMessage.getContactsStream(),
|
||||||
context.getSyncHelper()::handleSyncDeviceContacts);
|
input -> context.getSyncHelper()
|
||||||
|
.handleSyncDeviceContacts(input, receiveConfig.ignoreAvatars()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage());
|
logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
@ -576,7 +670,7 @@ public final class IncomingMessageHandler {
|
|||||||
final var sticker = context.getStickerHelper()
|
final var sticker = context.getStickerHelper()
|
||||||
.addOrUpdateStickerPack(stickerPackId, stickerPackKey, installed);
|
.addOrUpdateStickerPack(stickerPackId, stickerPackKey, installed);
|
||||||
|
|
||||||
if (sticker != null && installed) {
|
if (sticker != null && installed && !receiveConfig.ignoreStickers()) {
|
||||||
context.getJobExecutor().enqueueJob(new RetrieveStickerPackJob(stickerPackId, sticker.packKey()));
|
context.getJobExecutor().enqueueJob(new RetrieveStickerPackJob(stickerPackId, sticker.packKey()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -593,10 +687,6 @@ public final class IncomingMessageHandler {
|
|||||||
final var aep = keysMessage.getAccountEntropyPool();
|
final var aep = keysMessage.getAccountEntropyPool();
|
||||||
account.setAccountEntropyPool(aep);
|
account.setAccountEntropyPool(aep);
|
||||||
actions.add(SyncStorageDataAction.create());
|
actions.add(SyncStorageDataAction.create());
|
||||||
} else if (keysMessage.getMaster() != null) {
|
|
||||||
final var masterKey = keysMessage.getMaster();
|
|
||||||
account.setMasterKey(masterKey);
|
|
||||||
actions.add(SyncStorageDataAction.create());
|
|
||||||
} else if (keysMessage.getStorageService() != null) {
|
} else if (keysMessage.getStorageService() != null) {
|
||||||
final var storageKey = keysMessage.getStorageService();
|
final var storageKey = keysMessage.getStorageService();
|
||||||
account.setStorageKey(storageKey);
|
account.setStorageKey(storageKey);
|
||||||
@ -627,14 +717,39 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
if (syncMessage.getPniChangeNumber().isPresent()) {
|
if (syncMessage.getPniChangeNumber().isPresent()) {
|
||||||
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
||||||
logger.debug("Received PNI change number sync message, applying.");
|
if (account.isPrimaryDevice()) {
|
||||||
final var updatedPniString = envelope.getUpdatedPni();
|
logger.warn("Received PNI change number sync message on primary device, ignoring.");
|
||||||
if (updatedPniString != null && !updatedPniString.isEmpty()) {
|
} else if (sender.deviceId() != SignalServiceAddress.DEFAULT_DEVICE_ID) {
|
||||||
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
|
logger.warn("Received PNI change number sync message from non-primary device {}, ignoring.",
|
||||||
context.getAccountHelper().handlePniChangeNumberMessage(pniChangeNumber, updatedPni);
|
sender.deviceId());
|
||||||
|
} else {
|
||||||
|
final var envelopeServerTimestamp = envelope.getServerDeliveredTimestamp();
|
||||||
|
final var lastAppliedServerTimestamp = account.getLastAppliedPniChangeServerTimestamp();
|
||||||
|
if (envelopeServerTimestamp <= lastAppliedServerTimestamp) {
|
||||||
|
logger.warn(
|
||||||
|
"PNI change number sync server timestamp ({}) is not newer than last applied ({}), treating as replay.",
|
||||||
|
envelopeServerTimestamp,
|
||||||
|
lastAppliedServerTimestamp);
|
||||||
|
} else {
|
||||||
|
final var updatedPniString = envelope.getUpdatedPni();
|
||||||
|
if (updatedPniString != null && !updatedPniString.isEmpty()) {
|
||||||
|
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
|
||||||
|
final var applied = context.getAccountHelper()
|
||||||
|
.handlePniChangeNumberMessage(pniChangeNumber, updatedPni, true);
|
||||||
|
if (applied) {
|
||||||
|
account.setLastAppliedPniChangeServerTimestamp(envelopeServerTimestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actions;
|
if (syncMessage.getDeviceNameChange().isPresent()) {
|
||||||
|
final var deviceNameChange = syncMessage.getDeviceNameChange().get();
|
||||||
|
if (deviceNameChange.deviceId != null && deviceNameChange.deviceId == account.getDeviceId()) {
|
||||||
|
actions.add(RetrieveDeviceNameAction.create());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Pair<>(actions, longTexts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceGroupContext getGroupContext(SignalServiceContent content) {
|
private SignalServiceGroupContext getGroupContext(SignalServiceContent content) {
|
||||||
@ -700,21 +815,36 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupId = GroupUtils.getGroupId(groupContext);
|
final var message = content.getDataMessage().orElse(null);
|
||||||
var group = context.getGroupHelper().getGroup(groupId);
|
|
||||||
|
final var recipientId = account.getRecipientResolver().resolveRecipient(source);
|
||||||
|
|
||||||
|
final var groupId = GroupUtils.getGroupId(groupContext);
|
||||||
|
final var group = context.getGroupHelper().getGroup(groupId);
|
||||||
|
|
||||||
|
if (message != null && message.getAdminDelete().isPresent() && (group == null || !group.isAdmin(recipientId))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final var message = content.getDataMessage().orElse(null);
|
|
||||||
|
|
||||||
final var recipientId = account.getRecipientResolver().resolveRecipient(source);
|
|
||||||
if (!group.isMember(recipientId) && !(
|
if (!group.isMember(recipientId) && !(
|
||||||
group.isPendingMember(recipientId) && message != null && message.isGroupV2Update()
|
group.isPendingMember(recipientId) && message != null && message.isGroupV2Update()
|
||||||
)) {
|
)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (group.isTerminated()) {
|
||||||
|
return message == null
|
||||||
|
|| message.getBody().isPresent()
|
||||||
|
|| message.getAttachments().isPresent()
|
||||||
|
|| message.getQuote().isPresent()
|
||||||
|
|| message.getPreviews().isPresent()
|
||||||
|
|| message.getMentions().isPresent()
|
||||||
|
|| message.getSticker().isPresent();
|
||||||
|
}
|
||||||
if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
|
if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
|
||||||
return message == null
|
return message == null
|
||||||
|| message.getBody().isPresent()
|
|| message.getBody().isPresent()
|
||||||
@ -727,13 +857,14 @@ public final class IncomingMessageHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HandleAction> handleSignalServiceDataMessage(
|
private Pair<List<HandleAction>, Map<String, String>> handleSignalServiceDataMessage(
|
||||||
SignalServiceDataMessage message,
|
SignalServiceDataMessage message,
|
||||||
boolean isSync,
|
boolean isSync,
|
||||||
DeviceAddress source,
|
DeviceAddress source,
|
||||||
DeviceAddress destination,
|
DeviceAddress destination,
|
||||||
boolean ignoreAttachments
|
ReceiveConfig receiveConfig
|
||||||
) {
|
) {
|
||||||
|
final var longTexts = new HashMap<String, String>();
|
||||||
var actions = new ArrayList<HandleAction>();
|
var actions = new ArrayList<HandleAction>();
|
||||||
if (message.getGroupContext().isPresent()) {
|
if (message.getGroupContext().isPresent()) {
|
||||||
final var groupContext = message.getGroupContext().get();
|
final var groupContext = message.getGroupContext().get();
|
||||||
@ -749,7 +880,7 @@ public final class IncomingMessageHandler {
|
|||||||
groupV1 = new GroupInfoV1(groupId);
|
groupV1 = new GroupInfoV1(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupInfo.getAvatar().isPresent()) {
|
if (groupInfo.getAvatar().isPresent() && !receiveConfig.ignoreAvatars()) {
|
||||||
var avatar = groupInfo.getAvatar().get();
|
var avatar = groupInfo.getAvatar().get();
|
||||||
context.getGroupHelper().downloadGroupAvatar(groupV1.getGroupId(), avatar);
|
context.getGroupHelper().downloadGroupAvatar(groupV1.getGroupId(), avatar);
|
||||||
}
|
}
|
||||||
@ -791,17 +922,12 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (groupContext.getGroupV2().isPresent()) {
|
if (groupContext.getGroupV2().isPresent()) {
|
||||||
handleGroupV2Context(groupContext.getGroupV2().get());
|
handleGroupV2Context(groupContext.getGroupV2().get(), receiveConfig.ignoreAvatars());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var selfAddress = isSync ? source : destination;
|
final var selfAddress = isSync ? source : destination;
|
||||||
final var conversationPartnerAddress = isSync ? destination : source;
|
final var conversationPartnerAddress = isSync ? destination : source;
|
||||||
if (conversationPartnerAddress != null && message.isEndSession()) {
|
|
||||||
account.getAccountData(selfAddress.serviceId())
|
|
||||||
.getSessionStore()
|
|
||||||
.deleteAllSessions(conversationPartnerAddress.serviceId());
|
|
||||||
}
|
|
||||||
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
|
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
|
||||||
if (message.getGroupContext().isPresent()) {
|
if (message.getGroupContext().isPresent()) {
|
||||||
final var groupContext = message.getGroupContext().get();
|
final var groupContext = message.getGroupContext().get();
|
||||||
@ -824,10 +950,21 @@ public final class IncomingMessageHandler {
|
|||||||
message.getExpireTimerVersion());
|
message.getExpireTimerVersion());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!ignoreAttachments) {
|
if (!receiveConfig.ignoreAttachments()) {
|
||||||
if (message.getAttachments().isPresent()) {
|
if (message.getAttachments().isPresent()) {
|
||||||
for (var attachment : message.getAttachments().get()) {
|
for (var attachment : message.getAttachments().get()) {
|
||||||
context.getAttachmentHelper().downloadAttachment(attachment);
|
context.getAttachmentHelper().downloadAttachment(attachment);
|
||||||
|
if (attachment.isPointer()) {
|
||||||
|
final var file = context.getAttachmentHelper().getAttachmentFile(attachment.asPointer());
|
||||||
|
if (MimeUtils.LONG_TEXT.equals(attachment.getContentType()) && attachment.isPointer()) {
|
||||||
|
try {
|
||||||
|
final var longText = Files.readString(file.toPath());
|
||||||
|
longTexts.put(attachment.asPointer().getRemoteId().toString(), longText);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to read long text attachment, ignoring", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (message.getSharedContacts().isPresent()) {
|
if (message.getSharedContacts().isPresent()) {
|
||||||
@ -857,6 +994,21 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (message.getAttachments().isPresent()) {
|
||||||
|
for (var attachment : message.getAttachments().get()) {
|
||||||
|
if (MimeUtils.LONG_TEXT.equals(attachment.getContentType()) && attachment.isPointer()) {
|
||||||
|
try {
|
||||||
|
context.getAttachmentHelper().retrieveAttachment(attachment, in -> {
|
||||||
|
final var longText = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
longTexts.put(attachment.asPointer().getRemoteId().toString(), longText);
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to download long text attachment, ignoring", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (message.getGiftBadge().isPresent()) {
|
if (message.getGiftBadge().isPresent()) {
|
||||||
handleIncomingGiftBadge(message.getGiftBadge().get());
|
handleIncomingGiftBadge(message.getGiftBadge().get());
|
||||||
@ -872,9 +1024,12 @@ public final class IncomingMessageHandler {
|
|||||||
sticker = new StickerPack(stickerPackId, messageSticker.getPackKey());
|
sticker = new StickerPack(stickerPackId, messageSticker.getPackKey());
|
||||||
account.getStickerStore().addStickerPack(sticker);
|
account.getStickerStore().addStickerPack(sticker);
|
||||||
}
|
}
|
||||||
context.getJobExecutor().enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
|
if (!receiveConfig.ignoreStickers()) {
|
||||||
|
context.getJobExecutor()
|
||||||
|
.enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return actions;
|
return new Pair<>(actions, longTexts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleIncomingGiftBadge(final SignalServiceDataMessage.GiftBadge giftBadge) {
|
private void handleIncomingGiftBadge(final SignalServiceDataMessage.GiftBadge giftBadge) {
|
||||||
@ -884,14 +1039,14 @@ public final class IncomingMessageHandler {
|
|||||||
private List<HandleAction> handleSignalServiceStoryMessage(
|
private List<HandleAction> handleSignalServiceStoryMessage(
|
||||||
SignalServiceStoryMessage message,
|
SignalServiceStoryMessage message,
|
||||||
RecipientId source,
|
RecipientId source,
|
||||||
boolean ignoreAttachments
|
ReceiveConfig receiveConfig
|
||||||
) {
|
) {
|
||||||
var actions = new ArrayList<HandleAction>();
|
var actions = new ArrayList<HandleAction>();
|
||||||
if (message.getGroupContext().isPresent()) {
|
if (message.getGroupContext().isPresent()) {
|
||||||
handleGroupV2Context(message.getGroupContext().get());
|
handleGroupV2Context(message.getGroupContext().get(), receiveConfig.ignoreAvatars());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ignoreAttachments) {
|
if (!receiveConfig.ignoreAttachments()) {
|
||||||
if (message.getFileAttachment().isPresent()) {
|
if (message.getFileAttachment().isPresent()) {
|
||||||
context.getAttachmentHelper().downloadAttachment(message.getFileAttachment().get());
|
context.getAttachmentHelper().downloadAttachment(message.getFileAttachment().get());
|
||||||
}
|
}
|
||||||
@ -913,13 +1068,14 @@ public final class IncomingMessageHandler {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleGroupV2Context(final SignalServiceGroupV2 groupContext) {
|
private void handleGroupV2Context(final SignalServiceGroupV2 groupContext, final boolean ignoreAvatars) {
|
||||||
final var groupMasterKey = groupContext.getMasterKey();
|
final var groupMasterKey = groupContext.getMasterKey();
|
||||||
|
|
||||||
context.getGroupHelper()
|
context.getGroupHelper()
|
||||||
.getOrMigrateGroup(groupMasterKey,
|
.getOrMigrateGroup(groupMasterKey,
|
||||||
groupContext.getRevision(),
|
groupContext.getRevision(),
|
||||||
groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null);
|
groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null,
|
||||||
|
ignoreAvatars);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleIncomingProfileKey(final byte[] profileKeyBytes, final RecipientId source) {
|
private void handleIncomingProfileKey(final byte[] profileKeyBytes, final RecipientId source) {
|
||||||
@ -940,7 +1096,7 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||||
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
|
final var serviceId = envelope.getSourceServiceId();
|
||||||
if (!envelope.isUnidentifiedSender() && serviceId != null) {
|
if (!envelope.isUnidentifiedSender() && serviceId != null) {
|
||||||
return new SignalServiceAddress(serviceId);
|
return new SignalServiceAddress(serviceId);
|
||||||
} else if (content != null) {
|
} else if (content != null) {
|
||||||
@ -951,7 +1107,7 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||||
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
|
final var serviceId = envelope.getSourceServiceId();
|
||||||
if (!envelope.isUnidentifiedSender() && serviceId != null) {
|
if (!envelope.isUnidentifiedSender() && serviceId != null) {
|
||||||
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId),
|
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId),
|
||||||
serviceId,
|
serviceId,
|
||||||
@ -963,10 +1119,13 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DeviceAddress getDestination(SignalServiceEnvelope envelope) {
|
private DeviceAddress getDestination(SignalServiceEnvelope envelope) throws InvalidMessageException {
|
||||||
final var destination = envelope.getDestinationServiceId();
|
final var destination = envelope.getDestinationServiceId();
|
||||||
if (destination == null || destination.isUnknown()) {
|
if (destination == null || destination.isUnknown()) {
|
||||||
return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
|
throw new InvalidMessageException("Missing destination");
|
||||||
|
}
|
||||||
|
if (!account.getAci().equals(destination) && !account.getPni().equals(destination)) {
|
||||||
|
throw new InvalidMessageException("Message not intended for this account");
|
||||||
}
|
}
|
||||||
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
|
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
|
||||||
destination,
|
destination,
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||||
|
import org.signal.core.models.MasterKey;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
|
||||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||||
import org.whispersystems.signalservice.internal.push.AuthCredentials;
|
import org.whispersystems.signalservice.internal.push.AuthCredentials;
|
||||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||||
@ -23,11 +23,15 @@ public class PinHelper {
|
|||||||
|
|
||||||
public void setRegistrationLockPin(String pin, MasterKey masterKey) throws IOException {
|
public void setRegistrationLockPin(String pin, MasterKey masterKey) throws IOException {
|
||||||
IOException exception = null;
|
IOException exception = null;
|
||||||
|
var setPinSuccessfully = false;
|
||||||
for (final var secureValueRecovery : secureValueRecoveries) {
|
for (final var secureValueRecovery : secureValueRecoveries) {
|
||||||
try {
|
try {
|
||||||
final var backupResponse = secureValueRecovery.setPin(pin, masterKey).execute();
|
final var backupResponse = secureValueRecovery.setPin(pin, masterKey).execute();
|
||||||
switch (backupResponse) {
|
switch (backupResponse) {
|
||||||
case SecureValueRecovery.BackupResponse.Success success -> logger.trace("PIN set successfully");
|
case SecureValueRecovery.BackupResponse.Success success -> {
|
||||||
|
setPinSuccessfully = true;
|
||||||
|
logger.trace("PIN set successfully");
|
||||||
|
}
|
||||||
case SecureValueRecovery.BackupResponse.ServerRejected serverRejected ->
|
case SecureValueRecovery.BackupResponse.ServerRejected serverRejected ->
|
||||||
logger.warn("Backup svr failed: ServerRejected");
|
logger.warn("Backup svr failed: ServerRejected");
|
||||||
case SecureValueRecovery.BackupResponse.EnclaveNotFound enclaveNotFound ->
|
case SecureValueRecovery.BackupResponse.EnclaveNotFound enclaveNotFound ->
|
||||||
@ -43,7 +47,7 @@ public class PinHelper {
|
|||||||
exception = e;
|
exception = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exception != null) {
|
if (!setPinSuccessfully && exception != null) {
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,11 +58,13 @@ public class PinHelper {
|
|||||||
|
|
||||||
public void removeRegistrationLockPin() throws IOException {
|
public void removeRegistrationLockPin() throws IOException {
|
||||||
IOException exception = null;
|
IOException exception = null;
|
||||||
|
var removedPinSuccessfully = false;
|
||||||
for (final var secureValueRecovery : secureValueRecoveries) {
|
for (final var secureValueRecovery : secureValueRecoveries) {
|
||||||
try {
|
try {
|
||||||
final var deleteResponse = secureValueRecovery.deleteData();
|
final var deleteResponse = secureValueRecovery.deleteData();
|
||||||
switch (deleteResponse) {
|
switch (deleteResponse) {
|
||||||
case SecureValueRecovery.DeleteResponse.Success success -> {
|
case SecureValueRecovery.DeleteResponse.Success success -> {
|
||||||
|
removedPinSuccessfully = true;
|
||||||
}
|
}
|
||||||
case SecureValueRecovery.DeleteResponse.ServerRejected serverRejected ->
|
case SecureValueRecovery.DeleteResponse.ServerRejected serverRejected ->
|
||||||
logger.warn("Delete svr2 failed: ServerRejected");
|
logger.warn("Delete svr2 failed: ServerRejected");
|
||||||
@ -73,7 +79,7 @@ public class PinHelper {
|
|||||||
exception = e;
|
exception = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exception != null) {
|
if (!removedPinSuccessfully && exception != null) {
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.InvalidKeyIdException;
|
|||||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
||||||
import org.signal.libsignal.protocol.state.PreKeyRecord;
|
import org.signal.libsignal.protocol.state.PreKeyRecord;
|
||||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||||
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||||
@ -16,7 +17,6 @@ import org.whispersystems.signalservice.api.account.PreKeyUpload;
|
|||||||
import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts;
|
import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -84,7 +84,8 @@ public class PreKeyHelper {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
OneTimePreKeyCounts preKeyCounts;
|
OneTimePreKeyCounts preKeyCounts;
|
||||||
try {
|
try {
|
||||||
preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
|
preKeyCounts = handleResponseException(dependencies.getKeysApi()
|
||||||
|
.getAvailablePreKeyCountsSync(serviceIdType));
|
||||||
} catch (AuthorizationFailedException e) {
|
} catch (AuthorizationFailedException e) {
|
||||||
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
|
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
|
||||||
preKeyCounts = new OneTimePreKeyCounts(0, 0);
|
preKeyCounts = new OneTimePreKeyCounts(0, 0);
|
||||||
@ -145,7 +146,7 @@ public class PreKeyHelper {
|
|||||||
kyberPreKeyRecords);
|
kyberPreKeyRecords);
|
||||||
var needsReset = false;
|
var needsReset = false;
|
||||||
try {
|
try {
|
||||||
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
|
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeysSync(preKeyUpload));
|
||||||
try {
|
try {
|
||||||
if (preKeyRecords != null) {
|
if (preKeyRecords != null) {
|
||||||
account.addPreKeys(serviceIdType, preKeyRecords);
|
account.addPreKeys(serviceIdType, preKeyRecords);
|
||||||
|
|||||||
@ -17,10 +17,12 @@ import org.asamk.signal.manager.util.PaymentUtils;
|
|||||||
import org.asamk.signal.manager.util.ProfileUtils;
|
import org.asamk.signal.manager.util.ProfileUtils;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.signal.core.util.ExpiringProfileCredentialUtil;
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.network.exceptions.PushNetworkException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||||
@ -30,9 +32,7 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
|||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
|
||||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||||
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@ -116,7 +116,8 @@ public final class ProfileHelper {
|
|||||||
.filter(recipientId -> !ExpiringProfileCredentialUtil.isValid(account.getProfileStore()
|
.filter(recipientId -> !ExpiringProfileCredentialUtil.isValid(account.getProfileStore()
|
||||||
.getExpiringProfileKeyCredential(recipientId)))
|
.getExpiringProfileKeyCredential(recipientId)))
|
||||||
.map(recipientId -> retrieveProfile(recipientId,
|
.map(recipientId -> retrieveProfile(recipientId,
|
||||||
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL).onErrorComplete());
|
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL,
|
||||||
|
false).onErrorComplete());
|
||||||
Maybe.merge(profileFetches, 10).blockingSubscribe();
|
Maybe.merge(profileFetches, 10).blockingSubscribe();
|
||||||
|
|
||||||
return recipientIds.stream().map(r -> account.getProfileStore().getExpiringProfileKeyCredential(r)).toList();
|
return recipientIds.stream().map(r -> account.getProfileStore().getExpiringProfileKeyCredential(r)).toList();
|
||||||
@ -129,9 +130,13 @@ public final class ProfileHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL));
|
blockingGetProfile(retrieveProfile(recipientId,
|
||||||
|
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL,
|
||||||
|
false));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
|
logger.warn("Failed to retrieve profile key credential for {}, ignoring: {}",
|
||||||
|
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
||||||
|
e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,9 +193,11 @@ public final class ProfileHelper {
|
|||||||
if (uploadProfile) {
|
if (uploadProfile) {
|
||||||
final var streamDetails = avatar != null && avatar.isPresent()
|
final var streamDetails = avatar != null && avatar.isPresent()
|
||||||
? Utils.createStreamDetails(avatar.get())
|
? Utils.createStreamDetails(avatar.get())
|
||||||
.first()
|
.first()
|
||||||
: forceUploadAvatar && avatar == null ? context.getAvatarStore()
|
: forceUploadAvatar && avatar == null
|
||||||
.retrieveProfileAvatar(account.getSelfRecipientAddress()) : null;
|
? context.getAvatarStore()
|
||||||
|
.retrieveProfileAvatar(account.getSelfRecipientAddress())
|
||||||
|
: null;
|
||||||
try (streamDetails) {
|
try (streamDetails) {
|
||||||
final var avatarUploadParams = streamDetails != null
|
final var avatarUploadParams = streamDetails != null
|
||||||
? AvatarUploadParams.forAvatar(streamDetails)
|
? AvatarUploadParams.forAvatar(streamDetails)
|
||||||
@ -241,7 +248,8 @@ public final class ProfileHelper {
|
|||||||
final var profileFetches = Flowable.fromIterable(recipientIds)
|
final var profileFetches = Flowable.fromIterable(recipientIds)
|
||||||
.filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId)))
|
.filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId)))
|
||||||
.map(recipientId -> retrieveProfile(recipientId,
|
.map(recipientId -> retrieveProfile(recipientId,
|
||||||
SignalServiceProfile.RequestType.PROFILE).onErrorComplete());
|
SignalServiceProfile.RequestType.PROFILE,
|
||||||
|
false).onErrorComplete());
|
||||||
Maybe.merge(profileFetches, 10).blockingSubscribe();
|
Maybe.merge(profileFetches, 10).blockingSubscribe();
|
||||||
|
|
||||||
return recipientIds.stream().map(profileStore::getProfile).toList();
|
return recipientIds.stream().map(profileStore::getProfile).toList();
|
||||||
@ -255,9 +263,11 @@ public final class ProfileHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE));
|
blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE, false));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
logger.warn("Failed to retrieve profile for {}, ignoring: {}",
|
||||||
|
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
||||||
|
e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return account.getProfileStore().getProfile(recipientId);
|
return account.getProfileStore().getProfile(recipientId);
|
||||||
@ -272,17 +282,6 @@ public final class ProfileHelper {
|
|||||||
return now - profile.getLastUpdateTimestamp() >= 6 * 60 * 60 * 1000;
|
return now - profile.getLastUpdateTimestamp() >= 6 * 60 * 60 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Profile decryptProfileAndDownloadAvatar(
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final ProfileKey profileKey,
|
|
||||||
final SignalServiceProfile encryptedProfile
|
|
||||||
) {
|
|
||||||
final var avatarPath = encryptedProfile.getAvatar();
|
|
||||||
downloadProfileAvatar(recipientId, avatarPath, profileKey);
|
|
||||||
|
|
||||||
return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void downloadProfileAvatar(
|
public void downloadProfileAvatar(
|
||||||
final RecipientId recipientId,
|
final RecipientId recipientId,
|
||||||
final String avatarPath,
|
final String avatarPath,
|
||||||
@ -315,7 +314,8 @@ public final class ProfileHelper {
|
|||||||
|
|
||||||
private Single<ProfileAndCredential> retrieveProfile(
|
private Single<ProfileAndCredential> retrieveProfile(
|
||||||
RecipientId recipientId,
|
RecipientId recipientId,
|
||||||
SignalServiceProfile.RequestType requestType
|
SignalServiceProfile.RequestType requestType,
|
||||||
|
final boolean ignoreAvatars
|
||||||
) {
|
) {
|
||||||
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
|
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
|
||||||
var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId));
|
var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId));
|
||||||
@ -341,7 +341,12 @@ public final class ProfileHelper {
|
|||||||
Profile newProfile = null;
|
Profile newProfile = null;
|
||||||
if (profileKey.isPresent()) {
|
if (profileKey.isPresent()) {
|
||||||
logger.trace("Decrypting profile");
|
logger.trace("Decrypting profile");
|
||||||
newProfile = decryptProfileAndDownloadAvatar(recipientId, profileKey.get(), encryptedProfile);
|
final var avatarPath = encryptedProfile.getAvatar();
|
||||||
|
if (!ignoreAvatars) {
|
||||||
|
downloadProfileAvatar(recipientId, avatarPath, profileKey.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
newProfile = ProfileUtils.decryptProfile(profileKey.get(), encryptedProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newProfile == null) {
|
if (newProfile == null) {
|
||||||
@ -380,7 +385,9 @@ public final class ProfileHelper {
|
|||||||
|
|
||||||
logger.trace("Done handling retrieved profile");
|
logger.trace("Done handling retrieved profile");
|
||||||
}).doOnError(e -> {
|
}).doOnError(e -> {
|
||||||
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
logger.warn("Failed to retrieve profile for {}, ignoring: {}",
|
||||||
|
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
||||||
|
e.getMessage());
|
||||||
final var profile = account.getProfileStore().getProfile(recipientId);
|
final var profile = account.getProfileStore().getProfile(recipientId);
|
||||||
final var newProfile = (
|
final var newProfile = (
|
||||||
profile == null ? Profile.newBuilder() : Profile.newBuilder(profile)
|
profile == null ? Profile.newBuilder() : Profile.newBuilder(profile)
|
||||||
|
|||||||
@ -9,18 +9,17 @@ import org.asamk.signal.manager.jobs.CleanOldPreKeysJob;
|
|||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.messageCache.CachedMessage;
|
import org.asamk.signal.manager.storage.messageCache.CachedMessage;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.messages.EnvelopeResponse;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
|
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -41,7 +40,7 @@ public class ReceiveHelper {
|
|||||||
private final SignalDependencies dependencies;
|
private final SignalDependencies dependencies;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
private ReceiveConfig receiveConfig = new ReceiveConfig(false, false, false);
|
private ReceiveConfig receiveConfig = new ReceiveConfig(false, false, false, false, false);
|
||||||
private boolean hasCaughtUpWithOldMessages = false;
|
private boolean hasCaughtUpWithOldMessages = false;
|
||||||
private boolean isWaitingForMessage = false;
|
private boolean isWaitingForMessage = false;
|
||||||
private boolean shouldStop = false;
|
private boolean shouldStop = false;
|
||||||
@ -123,7 +122,6 @@ public class ReceiveHelper {
|
|||||||
Manager.ReceiveMessageHandler handler,
|
Manager.ReceiveMessageHandler handler,
|
||||||
final Map<HandleAction, HandleAction> queuedActions
|
final Map<HandleAction, HandleAction> queuedActions
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var timeoutInstant = timeout.map(t -> Instant.now().plus(t));
|
|
||||||
int remainingMessages = maxMessages == null ? -1 : maxMessages;
|
int remainingMessages = maxMessages == null ? -1 : maxMessages;
|
||||||
var backOffCounter = 0;
|
var backOffCounter = 0;
|
||||||
isWaitingForMessage = false;
|
isWaitingForMessage = false;
|
||||||
@ -142,8 +140,7 @@ public class ReceiveHelper {
|
|||||||
logger.trace("Checking for new message from server");
|
logger.trace("Checking for new message from server");
|
||||||
try {
|
try {
|
||||||
isWaitingForMessage = true;
|
isWaitingForMessage = true;
|
||||||
final var timeoutMs = timeoutInstant.isPresent() ? Math.min(60_000,
|
final var timeoutMs = timeout.orElseGet(() -> Duration.ofMinutes(1)).toMillis();
|
||||||
Duration.between(Instant.now(), timeoutInstant.get()).toMillis()) : 60_000L;
|
|
||||||
if (timeoutMs <= 0L) {
|
if (timeoutMs <= 0L) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -151,15 +148,19 @@ public class ReceiveHelper {
|
|||||||
logger.debug("Retrieved {} envelopes!", batch.size());
|
logger.debug("Retrieved {} envelopes!", batch.size());
|
||||||
isWaitingForMessage = false;
|
isWaitingForMessage = false;
|
||||||
for (final var it : batch) {
|
for (final var it : batch) {
|
||||||
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
|
if (it instanceof EnvelopeResponse.Unparseable) {
|
||||||
it.getServerDeliveredTimestamp());
|
logger.warn("Received unparseable envelope from server, ignoring.");
|
||||||
final var recipientId = envelope1.getSourceServiceId()
|
} else if (it instanceof EnvelopeResponse.Parsed parsed) {
|
||||||
.map(ServiceId::parseOrNull)
|
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(parsed.getEnvelope(),
|
||||||
.map(s -> account.getRecipientResolver().resolveRecipient(s))
|
parsed.getServerDeliveredTimestamp());
|
||||||
.orElse(null);
|
final var sourceServiceId = envelope1.getSourceServiceId();
|
||||||
logger.trace("Storing new message from {}", recipientId);
|
final var recipientId = sourceServiceId == null
|
||||||
// store message on disk, before acknowledging receipt to the server
|
? null
|
||||||
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
: account.getRecipientResolver().resolveRecipient(sourceServiceId);
|
||||||
|
logger.trace("Storing new message from {}", recipientId);
|
||||||
|
// store message on disk, before acknowledging receipt to the server
|
||||||
|
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
signalWebSocket.sendAck(it);
|
signalWebSocket.sendAck(it);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -171,6 +172,9 @@ public class ReceiveHelper {
|
|||||||
backOffCounter = 0;
|
backOffCounter = 0;
|
||||||
|
|
||||||
if (queueNotEmpty) {
|
if (queueNotEmpty) {
|
||||||
|
if (cachedMessage[0] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (remainingMessages > 0) {
|
if (remainingMessages > 0) {
|
||||||
remainingMessages -= 1;
|
remainingMessages -= 1;
|
||||||
}
|
}
|
||||||
@ -212,7 +216,7 @@ public class ReceiveHelper {
|
|||||||
throw e;
|
throw e;
|
||||||
} catch (TimeoutException e) {
|
} catch (TimeoutException e) {
|
||||||
backOffCounter = 0;
|
backOffCounter = 0;
|
||||||
if (timeoutInstant.isPresent() && timeoutInstant.get().isBefore(Instant.now())) {
|
if (timeout.isPresent()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@ -241,7 +245,7 @@ public class ReceiveHelper {
|
|||||||
if (exception instanceof UntrustedIdentityException) {
|
if (exception instanceof UntrustedIdentityException) {
|
||||||
logger.debug("Keeping message with untrusted identity in message cache");
|
logger.debug("Keeping message with untrusted identity in message cache");
|
||||||
final var address = ((UntrustedIdentityException) exception).getSender();
|
final var address = ((UntrustedIdentityException) exception).getSender();
|
||||||
if (envelope.getSourceServiceId().isEmpty() && address.aci().isPresent()) {
|
if (envelope.getSourceServiceId() == null && address.aci().isPresent()) {
|
||||||
final var recipientId = account.getRecipientResolver()
|
final var recipientId = account.getRecipientResolver()
|
||||||
.resolveRecipient(ACI.parseOrThrow(address.aci().get()));
|
.resolveRecipient(ACI.parseOrThrow(address.aci().get()));
|
||||||
try {
|
try {
|
||||||
@ -295,7 +299,7 @@ public class ReceiveHelper {
|
|||||||
cachedMessage.delete();
|
cachedMessage.delete();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (envelope.getSourceServiceId().isEmpty()) {
|
if (envelope.getSourceServiceId() == null) {
|
||||||
final var identifier = ((UntrustedIdentityException) exception).getSender();
|
final var identifier = ((UntrustedIdentityException) exception).getSender();
|
||||||
final var recipientId = account.getRecipientResolver()
|
final var recipientId = account.getRecipientResolver()
|
||||||
.resolveRecipient(new RecipientAddress(identifier));
|
.resolveRecipient(new RecipientAddress(identifier));
|
||||||
|
|||||||
@ -6,18 +6,18 @@ import org.asamk.signal.manager.api.UsernameLinkUrl;
|
|||||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.signal.libsignal.usernames.Username;
|
import org.signal.libsignal.usernames.Username;
|
||||||
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
|
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@ -145,11 +145,8 @@ public class RecipientHelper {
|
|||||||
try {
|
try {
|
||||||
final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
|
final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
|
||||||
final var components = usernameLinkUrl.getComponents();
|
final var components = usernameLinkUrl.getComponents();
|
||||||
final var encryptedUsername = handleResponseException(dependencies.getUsernameApi()
|
return handleResponseException(dependencies.getUsernameApi()
|
||||||
.getEncryptedUsernameFromLinkServerId(components.getServerId()));
|
.getDecryptedUsernameFromLinkServerIdAndEntropy(components.getServerId(), components.getEntropy()));
|
||||||
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
|
|
||||||
|
|
||||||
return Username.fromLink(link);
|
|
||||||
} catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
|
} catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
|
||||||
return new Username(username);
|
return new Username(username);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import org.asamk.signal.manager.storage.groups.GroupInfo;
|
|||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
|
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.InvalidRegistrationIdException;
|
import org.signal.libsignal.protocol.InvalidRegistrationIdException;
|
||||||
import org.signal.libsignal.protocol.NoSessionException;
|
import org.signal.libsignal.protocol.NoSessionException;
|
||||||
@ -34,11 +35,13 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
|||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||||
@ -84,7 +87,8 @@ public class SendHelper {
|
|||||||
public SendMessageResult sendMessage(
|
public SendMessageResult sendMessage(
|
||||||
final SignalServiceDataMessage.Builder messageBuilder,
|
final SignalServiceDataMessage.Builder messageBuilder,
|
||||||
final RecipientId recipientId,
|
final RecipientId recipientId,
|
||||||
Optional<Long> editTargetTimestamp
|
Optional<Long> editTargetTimestamp,
|
||||||
|
boolean urgent
|
||||||
) {
|
) {
|
||||||
var contact = account.getContactStore().getContact(recipientId);
|
var contact = account.getContactStore().getContact(recipientId);
|
||||||
if (contact == null || !contact.isProfileSharingEnabled() || contact.isHidden()) {
|
if (contact == null || !contact.isProfileSharingEnabled() || contact.isHidden()) {
|
||||||
@ -102,7 +106,7 @@ public class SendHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final var message = messageBuilder.build();
|
final var message = messageBuilder.build();
|
||||||
return sendMessage(message, recipientId, editTargetTimestamp);
|
return sendMessage(message, recipientId, editTargetTimestamp, urgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,10 +117,11 @@ public class SendHelper {
|
|||||||
final SignalServiceDataMessage.Builder messageBuilder,
|
final SignalServiceDataMessage.Builder messageBuilder,
|
||||||
final GroupId groupId,
|
final GroupId groupId,
|
||||||
final boolean includeSelf,
|
final boolean includeSelf,
|
||||||
final Optional<Long> editTargetTimestamp
|
final Optional<Long> editTargetTimestamp,
|
||||||
|
boolean urgent
|
||||||
) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
||||||
final var g = getGroupForSending(groupId);
|
final var g = getGroupForSending(groupId);
|
||||||
return sendAsGroupMessage(messageBuilder, g, includeSelf, editTargetTimestamp);
|
return sendAsGroupMessage(messageBuilder, g, includeSelf, editTargetTimestamp, urgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,7 +133,7 @@ public class SendHelper {
|
|||||||
final Set<RecipientId> recipientIds,
|
final Set<RecipientId> recipientIds,
|
||||||
final GroupInfo groupInfo
|
final GroupInfo groupInfo
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
return sendGroupMessage(message, recipientIds, groupInfo, ContentHint.IMPLICIT, Optional.empty());
|
return sendGroupMessage(message, recipientIds, groupInfo, ContentHint.IMPLICIT, Optional.empty(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendReceiptMessage(
|
public SendMessageResult sendReceiptMessage(
|
||||||
@ -307,17 +312,125 @@ public class SendHelper {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SendMessageResult sendCallMessage(
|
||||||
|
final SignalServiceCallMessage callMessage,
|
||||||
|
final RecipientId recipientId
|
||||||
|
) {
|
||||||
|
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||||
|
final var result = handleSendMessage(recipientId,
|
||||||
|
(messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendCallMessage(
|
||||||
|
address,
|
||||||
|
unidentifiedAccess,
|
||||||
|
callMessage));
|
||||||
|
if (callMessage.getTimestamp().isPresent()) {
|
||||||
|
messageSendLogStore.insertIfPossible(callMessage.getTimestamp().get(),
|
||||||
|
result,
|
||||||
|
ContentHint.IMPLICIT,
|
||||||
|
callMessage.isUrgent());
|
||||||
|
}
|
||||||
|
handleSendMessageResult(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a story message (file attachment) to "My Story".
|
||||||
|
*/
|
||||||
|
public List<SendMessageResult> sendStoryMessage(
|
||||||
|
SignalServiceStoryMessage storyMessage,
|
||||||
|
long timestamp,
|
||||||
|
Set<RecipientId> recipientIds,
|
||||||
|
boolean allowsReplies
|
||||||
|
) throws IOException {
|
||||||
|
final var messageSender = dependencies.getMessageSender();
|
||||||
|
|
||||||
|
final var recipientIdList = List.copyOf(recipientIds);
|
||||||
|
final var addressesMap = recipientIdList.stream()
|
||||||
|
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
||||||
|
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds);
|
||||||
|
|
||||||
|
final var addresses = recipientIdList.stream().map(addressesMap::get).toList();
|
||||||
|
final var unidentifiedAccesses = recipientIdList.stream().map(unidentifiedAccessesMap::get).toList();
|
||||||
|
final var storyMessageRecipients = recipientIdList.stream()
|
||||||
|
.map(id -> new SignalServiceStoryMessageRecipient(addressesMap.get(id),
|
||||||
|
List.of(DistributionId.MY_STORY.asUuid().toString()),
|
||||||
|
allowsReplies))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
final List<SendMessageResult> results;
|
||||||
|
try {
|
||||||
|
results = messageSender.sendGroupStory(DistributionId.MY_STORY,
|
||||||
|
Optional.empty(),
|
||||||
|
addresses,
|
||||||
|
unidentifiedAccesses,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
storyMessage,
|
||||||
|
timestamp,
|
||||||
|
storyMessageRecipients,
|
||||||
|
null);
|
||||||
|
} catch (UntrustedIdentityException | InvalidKeyException | NoSessionException |
|
||||||
|
InvalidRegistrationIdException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var r : results) {
|
||||||
|
handleSendMessageResult(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a story message (file attachment) to a group.
|
||||||
|
*/
|
||||||
|
public List<SendMessageResult> sendGroupStoryMessage(
|
||||||
|
SignalServiceStoryMessage storyMessage,
|
||||||
|
long timestamp,
|
||||||
|
GroupInfoV2 groupInfo,
|
||||||
|
boolean allowsReplies
|
||||||
|
) throws IOException {
|
||||||
|
final var messageSender = dependencies.getMessageSender();
|
||||||
|
|
||||||
|
final var allRecipientIds = groupInfo.getMembersWithout(account.getSelfRecipientId());
|
||||||
|
|
||||||
|
final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupStory(
|
||||||
|
distId,
|
||||||
|
Optional.of(groupInfo.getMasterKey().serialize()),
|
||||||
|
recipients,
|
||||||
|
unidentifiedAccess,
|
||||||
|
groupSendEndorsements,
|
||||||
|
isRecipientUpdate,
|
||||||
|
storyMessage,
|
||||||
|
timestamp,
|
||||||
|
recipients.stream()
|
||||||
|
.map(address -> new SignalServiceStoryMessageRecipient(address,
|
||||||
|
List.of(groupInfo.getDistributionId().asUuid().toString()),
|
||||||
|
allowsReplies))
|
||||||
|
.collect(Collectors.toSet()),
|
||||||
|
null);
|
||||||
|
final var results = sendGroupMessageInternal(null, senderKeySender, allRecipientIds, groupInfo, false);
|
||||||
|
|
||||||
|
for (var r : results) {
|
||||||
|
handleSendMessageResult(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
private List<SendMessageResult> sendAsGroupMessage(
|
private List<SendMessageResult> sendAsGroupMessage(
|
||||||
final SignalServiceDataMessage.Builder messageBuilder,
|
final SignalServiceDataMessage.Builder messageBuilder,
|
||||||
final GroupInfo g,
|
final GroupInfo g,
|
||||||
final boolean includeSelf,
|
final boolean includeSelf,
|
||||||
final Optional<Long> editTargetTimestamp
|
final Optional<Long> editTargetTimestamp,
|
||||||
|
boolean urgent
|
||||||
) throws IOException, GroupSendingNotAllowedException {
|
) throws IOException, GroupSendingNotAllowedException {
|
||||||
GroupUtils.setGroupContext(messageBuilder, g);
|
GroupUtils.setGroupContext(messageBuilder, g);
|
||||||
messageBuilder.withExpiration(g.getMessageExpirationTimer());
|
messageBuilder.withExpiration(g.getMessageExpirationTimer());
|
||||||
|
|
||||||
final var message = messageBuilder.build();
|
final var message = messageBuilder.build();
|
||||||
final var recipients = includeSelf ? g.getMembers() : g.getMembersWithout(account.getSelfRecipientId());
|
final var recipients = includeSelf
|
||||||
|
? g.getMemberRecipientIds()
|
||||||
|
: g.getMembersWithout(account.getSelfRecipientId());
|
||||||
|
|
||||||
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
|
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
|
||||||
if (message.getBody().isPresent()
|
if (message.getBody().isPresent()
|
||||||
@ -330,7 +443,7 @@ public class SendHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendGroupMessage(message, recipients, g, ContentHint.RESENDABLE, editTargetTimestamp);
|
return sendGroupMessage(message, recipients, g, ContentHint.RESENDABLE, editTargetTimestamp, urgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SendMessageResult> sendGroupMessage(
|
private List<SendMessageResult> sendGroupMessage(
|
||||||
@ -338,13 +451,13 @@ public class SendHelper {
|
|||||||
final Set<RecipientId> recipientIds,
|
final Set<RecipientId> recipientIds,
|
||||||
final GroupInfo groupInfo,
|
final GroupInfo groupInfo,
|
||||||
final ContentHint contentHint,
|
final ContentHint contentHint,
|
||||||
final Optional<Long> editTargetTimestamp
|
final Optional<Long> editTargetTimestamp,
|
||||||
|
boolean urgent
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var messageSender = dependencies.getMessageSender();
|
final var messageSender = dependencies.getMessageSender();
|
||||||
final var messageSendLogStore = account.getMessageSendLogStore();
|
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||||
final AtomicLong entryId = new AtomicLong(-1);
|
final AtomicLong entryId = new AtomicLong(-1);
|
||||||
|
|
||||||
final var urgent = true;
|
|
||||||
final PartialSendCompleteListener partialSendCompleteListener = sendResult -> {
|
final PartialSendCompleteListener partialSendCompleteListener = sendResult -> {
|
||||||
logger.trace("Partial message send result: {}", sendResult.isSuccess());
|
logger.trace("Partial message send result: {}", sendResult.isSuccess());
|
||||||
synchronized (entryId) {
|
synchronized (entryId) {
|
||||||
@ -454,7 +567,7 @@ public class SendHelper {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
|
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
||||||
var g = context.getGroupHelper().getGroup(groupId);
|
var g = context.getGroupHelper().getGroup(groupId);
|
||||||
if (g == null) {
|
if (g == null) {
|
||||||
throw new GroupNotFoundException(groupId);
|
throw new GroupNotFoundException(groupId);
|
||||||
@ -462,6 +575,10 @@ public class SendHelper {
|
|||||||
if (!g.isMember(account.getSelfRecipientId())) {
|
if (!g.isMember(account.getSelfRecipientId())) {
|
||||||
throw new NotAGroupMemberException(groupId, g.getTitle());
|
throw new NotAGroupMemberException(groupId, g.getTitle());
|
||||||
}
|
}
|
||||||
|
if (g.isTerminated()) {
|
||||||
|
// Other clients drop messages sent to a terminated group.
|
||||||
|
throw new GroupSendingNotAllowedException(groupId, g.getTitle());
|
||||||
|
}
|
||||||
if (!g.isProfileSharingEnabled()) {
|
if (!g.isProfileSharingEnabled()) {
|
||||||
g.setProfileSharingEnabled(true);
|
g.setProfileSharingEnabled(true);
|
||||||
account.getGroupStore().updateGroup(g);
|
account.getGroupStore().updateGroup(g);
|
||||||
@ -481,9 +598,31 @@ public class SendHelper {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
final var addressesMap = recipientIds.stream()
|
// Recipients that are already known to be unregistered are skipped here.
|
||||||
|
// Otherwise every group send re-attempts them via the slow legacy 1:1
|
||||||
|
// fan-out, which on large groups can take tens of seconds (and time out).
|
||||||
|
// The unregistered flag is maintained independently by profile/CDS
|
||||||
|
// discovery, which clears it once a recipient registers again, so they
|
||||||
|
// are re-included automatically. An unregisteredFailure result is still
|
||||||
|
// returned for each skipped recipient, so callers see them unchanged.
|
||||||
|
final var skippedResults = new ArrayList<SendMessageResult>();
|
||||||
|
final Set<RecipientId> targetRecipientIds;
|
||||||
|
final var unregisteredRecipientIds = account.getRecipientStore().getUnregisteredRecipientIds(recipientIds);
|
||||||
|
if (unregisteredRecipientIds.isEmpty()) {
|
||||||
|
targetRecipientIds = recipientIds;
|
||||||
|
} else {
|
||||||
|
logger.debug("Skipping {} known-unregistered recipient(s) in group send.", unregisteredRecipientIds.size());
|
||||||
|
targetRecipientIds = new HashSet<>(recipientIds);
|
||||||
|
targetRecipientIds.removeAll(unregisteredRecipientIds);
|
||||||
|
for (final var recipientId : unregisteredRecipientIds) {
|
||||||
|
skippedResults.add(SendMessageResult.unregisteredFailure(context.getRecipientHelper()
|
||||||
|
.resolveSignalServiceAddress(recipientId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var addressesMap = targetRecipientIds.stream()
|
||||||
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
||||||
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds);
|
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(targetRecipientIds);
|
||||||
final var groupSendEndorsementsResult = getGroupSendEndorsements(groupInfo);
|
final var groupSendEndorsementsResult = getGroupSendEndorsements(groupInfo);
|
||||||
final var groupSecretParams = groupInfo instanceof GroupInfoV2 gv2
|
final var groupSecretParams = groupInfo instanceof GroupInfoV2 gv2
|
||||||
? GroupSecretParams.deriveFromMasterKey((gv2.getMasterKey()))
|
? GroupSecretParams.deriveFromMasterKey((gv2.getMasterKey()))
|
||||||
@ -497,17 +636,19 @@ public class SendHelper {
|
|||||||
: groupSendEndorsementsResult.first();
|
: groupSendEndorsementsResult.first();
|
||||||
Set<RecipientId> senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null
|
Set<RecipientId> senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null
|
||||||
? Set.of()
|
? Set.of()
|
||||||
: recipientIds.stream()
|
: targetRecipientIds.stream()
|
||||||
.filter(s -> this.isSenderKeyCapable(s,
|
.filter(s -> this.isSenderKeyCapable(s,
|
||||||
addressesMap.get(s),
|
addressesMap.get(s),
|
||||||
unidentifiedAccessesMap.get(s),
|
unidentifiedAccessesMap.get(s),
|
||||||
groupSendEndorsements))
|
groupSendEndorsements))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
if (senderKeyTargets.size() < 2) {
|
if (senderKeyTargets.size() < 2) {
|
||||||
logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
|
logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
|
||||||
senderKeyTargets = Set.of();
|
senderKeyTargets = Set.of();
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
|
logger.debug("Can use sender key for {}/{} recipients.",
|
||||||
|
senderKeyTargets.size(),
|
||||||
|
targetRecipientIds.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
|
final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
|
||||||
@ -543,11 +684,11 @@ public class SendHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var legacyTargets = new HashSet<>(recipientIds);
|
final var legacyTargets = new HashSet<>(targetRecipientIds);
|
||||||
legacyTargets.removeAll(senderKeyTargets);
|
legacyTargets.removeAll(senderKeyTargets);
|
||||||
final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
|
final boolean onlyTargetIsSelfWithLinkedDevice = targetRecipientIds.isEmpty() && account.isMultiDevice();
|
||||||
|
|
||||||
if (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice) {
|
if (legacySender != null && (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice)) {
|
||||||
if (!legacyTargets.isEmpty()) {
|
if (!legacyTargets.isEmpty()) {
|
||||||
logger.debug("Need to do {} legacy sends.", legacyTargets.size());
|
logger.debug("Need to do {} legacy sends.", legacyTargets.size());
|
||||||
} else {
|
} else {
|
||||||
@ -564,11 +705,11 @@ public class SendHelper {
|
|||||||
final var expirationMs = Instant.ofEpochMilli(groupSendEndorsementsExpirationMs);
|
final var expirationMs = Instant.ofEpochMilli(groupSendEndorsementsExpirationMs);
|
||||||
final var groupSendTokens = groupSendEndorsements != null && groupSecretParams != null
|
final var groupSendTokens = groupSendEndorsements != null && groupSecretParams != null
|
||||||
? legacyTargets.stream()
|
? legacyTargets.stream()
|
||||||
.map(groupSendEndorsements::get)
|
.map(groupSendEndorsements::get)
|
||||||
.map(endorsement -> Optional.ofNullable(endorsement)
|
.map(endorsement -> Optional.ofNullable(endorsement)
|
||||||
.map(e -> e.toFullToken(groupSecretParams, expirationMs))
|
.map(e -> e.toFullToken(groupSecretParams, expirationMs))
|
||||||
.orElse(null))
|
.orElse(null))
|
||||||
.toList()
|
.toList()
|
||||||
: null;
|
: null;
|
||||||
final var sealedSenderAccesses = SealedSenderAccess.forFanOutGroupSend(groupSendTokens,
|
final var sealedSenderAccesses = SealedSenderAccess.forFanOutGroupSend(groupSendTokens,
|
||||||
senderCertificate,
|
senderCertificate,
|
||||||
@ -579,6 +720,7 @@ public class SendHelper {
|
|||||||
isRecipientUpdate || !allResults.isEmpty());
|
isRecipientUpdate || !allResults.isEmpty());
|
||||||
allResults.addAll(results);
|
allResults.addAll(results);
|
||||||
}
|
}
|
||||||
|
allResults.addAll(skippedResults);
|
||||||
final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
|
final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
|
||||||
logger.debug("Sending took {}", duration.toString());
|
logger.debug("Sending took {}", duration.toString());
|
||||||
return allResults;
|
return allResults;
|
||||||
@ -641,7 +783,7 @@ public class SendHelper {
|
|||||||
final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
|
final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
|
||||||
logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, addresses.size());
|
logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, addresses.size());
|
||||||
return results;
|
return results;
|
||||||
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException | NoSessionException e) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -712,10 +854,10 @@ public class SendHelper {
|
|||||||
private SendMessageResult sendMessage(
|
private SendMessageResult sendMessage(
|
||||||
SignalServiceDataMessage message,
|
SignalServiceDataMessage message,
|
||||||
RecipientId recipientId,
|
RecipientId recipientId,
|
||||||
Optional<Long> editTargetTimestamp
|
Optional<Long> editTargetTimestamp,
|
||||||
|
boolean urgent
|
||||||
) {
|
) {
|
||||||
final var messageSendLogStore = account.getMessageSendLogStore();
|
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||||
final var urgent = true;
|
|
||||||
final var result = handleSendMessage(recipientId,
|
final var result = handleSendMessage(recipientId,
|
||||||
editTargetTimestamp.isEmpty()
|
editTargetTimestamp.isEmpty()
|
||||||
? (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendDataMessage(
|
? (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendDataMessage(
|
||||||
@ -833,7 +975,7 @@ public class SendHelper {
|
|||||||
SignalServiceAddress address,
|
SignalServiceAddress address,
|
||||||
SealedSenderAccess unidentifiedAccess,
|
SealedSenderAccess unidentifiedAccess,
|
||||||
boolean includePniSignature
|
boolean includePniSignature
|
||||||
) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException, NoSessionException;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SenderKeySenderHandler {
|
interface SenderKeySenderHandler {
|
||||||
@ -853,6 +995,6 @@ public class SendHelper {
|
|||||||
List<SignalServiceAddress> recipients,
|
List<SignalServiceAddress> recipients,
|
||||||
List<SealedSenderAccess> unidentifiedAccess,
|
List<SealedSenderAccess> unidentifiedAccess,
|
||||||
boolean isRecipientUpdate
|
boolean isRecipientUpdate
|
||||||
) throws IOException, UntrustedIdentityException;
|
) throws IOException, UntrustedIdentityException, NoSessionException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,10 +7,10 @@ import org.asamk.signal.manager.storage.SignalAccount;
|
|||||||
import org.asamk.signal.manager.storage.stickerPacks.JsonStickerPack;
|
import org.asamk.signal.manager.storage.stickerPacks.JsonStickerPack;
|
||||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
|
import org.signal.core.util.Hex;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|||||||
@ -2,20 +2,29 @@ package org.asamk.signal.manager.helper;
|
|||||||
|
|
||||||
import org.asamk.signal.manager.api.GroupIdV1;
|
import org.asamk.signal.manager.api.GroupIdV1;
|
||||||
import org.asamk.signal.manager.api.GroupIdV2;
|
import org.asamk.signal.manager.api.GroupIdV2;
|
||||||
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.asamk.signal.manager.api.Profile;
|
import org.asamk.signal.manager.api.Profile;
|
||||||
|
import org.asamk.signal.manager.api.StickerPackId;
|
||||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
||||||
import org.asamk.signal.manager.syncStorage.AccountRecordProcessor;
|
import org.asamk.signal.manager.syncStorage.AccountRecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.ContactRecordProcessor;
|
import org.asamk.signal.manager.syncStorage.ContactRecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor;
|
import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.GroupV2RecordProcessor;
|
import org.asamk.signal.manager.syncStorage.GroupV2RecordProcessor;
|
||||||
|
import org.asamk.signal.manager.syncStorage.StickerPackRecordProcessor;
|
||||||
|
import org.asamk.signal.manager.syncStorage.StorageSyncLoopDetector;
|
||||||
import org.asamk.signal.manager.syncStorage.StorageSyncModels;
|
import org.asamk.signal.manager.syncStorage.StorageSyncModels;
|
||||||
import org.asamk.signal.manager.syncStorage.StorageSyncValidations;
|
import org.asamk.signal.manager.syncStorage.StorageSyncValidations;
|
||||||
import org.asamk.signal.manager.syncStorage.WriteOperationResult;
|
import org.asamk.signal.manager.syncStorage.WriteOperationResult;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
|
import org.signal.core.models.storageservice.StorageKey;
|
||||||
import org.signal.core.util.SetUtil;
|
import org.signal.core.util.SetUtil;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
|
import org.signal.network.service.StorageServiceService;
|
||||||
|
import org.signal.network.service.StorageServiceService.ManifestIfDifferentVersionResult;
|
||||||
|
import org.signal.network.service.StorageServiceService.WriteStorageRecordsResult;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||||
@ -23,11 +32,7 @@ import org.whispersystems.signalservice.api.storage.RecordIkm;
|
|||||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt;
|
import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult;
|
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
||||||
|
|
||||||
@ -38,6 +43,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -50,16 +56,19 @@ public class StorageHelper {
|
|||||||
private static final List<Integer> KNOWN_TYPES = List.of(ManifestRecord.Identifier.Type.CONTACT.getValue(),
|
private static final List<Integer> KNOWN_TYPES = List.of(ManifestRecord.Identifier.Type.CONTACT.getValue(),
|
||||||
ManifestRecord.Identifier.Type.GROUPV1.getValue(),
|
ManifestRecord.Identifier.Type.GROUPV1.getValue(),
|
||||||
ManifestRecord.Identifier.Type.GROUPV2.getValue(),
|
ManifestRecord.Identifier.Type.GROUPV2.getValue(),
|
||||||
ManifestRecord.Identifier.Type.ACCOUNT.getValue());
|
ManifestRecord.Identifier.Type.ACCOUNT.getValue(),
|
||||||
|
ManifestRecord.Identifier.Type.STICKER_PACK.getValue());
|
||||||
|
|
||||||
private final SignalAccount account;
|
private final SignalAccount account;
|
||||||
private final SignalDependencies dependencies;
|
private final SignalDependencies dependencies;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
private final StorageSyncLoopDetector storageSyncLoopDetector;
|
||||||
|
|
||||||
public StorageHelper(final Context context) {
|
public StorageHelper(final Context context) {
|
||||||
this.account = context.getAccount();
|
this.account = context.getAccount();
|
||||||
this.dependencies = context.getDependencies();
|
this.dependencies = context.getDependencies();
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.storageSyncLoopDetector = new StorageSyncLoopDetector(account::isMultiDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void syncDataWithStorage() throws IOException {
|
public void syncDataWithStorage() throws IOException {
|
||||||
@ -78,6 +87,7 @@ public class StorageHelper {
|
|||||||
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
||||||
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
||||||
localManifestVersion);
|
localManifestVersion);
|
||||||
|
final var fetchedRemoteManifest = result instanceof ManifestIfDifferentVersionResult.DifferentVersion;
|
||||||
|
|
||||||
var needsForcePush = false;
|
var needsForcePush = false;
|
||||||
final var remoteManifest = switch (result) {
|
final var remoteManifest = switch (result) {
|
||||||
@ -118,6 +128,7 @@ public class StorageHelper {
|
|||||||
logger.trace("Adding missing storageIds to local data");
|
logger.trace("Adding missing storageIds to local data");
|
||||||
account.getRecipientStore().setMissingStorageIds();
|
account.getRecipientStore().setMissingStorageIds();
|
||||||
account.getGroupStore().setMissingStorageIds();
|
account.getGroupStore().setMissingStorageIds();
|
||||||
|
account.getStickerStore().setMissingStorageIds();
|
||||||
|
|
||||||
var needsMultiDeviceSync = false;
|
var needsMultiDeviceSync = false;
|
||||||
|
|
||||||
@ -139,7 +150,10 @@ public class StorageHelper {
|
|||||||
needsForcePush = true;
|
needsForcePush = true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
needsMultiDeviceSync = writeToStorage(storageKey,
|
||||||
|
remoteManifest,
|
||||||
|
needsForcePush,
|
||||||
|
fetchedRemoteManifest);
|
||||||
} catch (RetryLaterException e) {
|
} catch (RetryLaterException e) {
|
||||||
// TODO retry later
|
// TODO retry later
|
||||||
return;
|
return;
|
||||||
@ -211,19 +225,26 @@ public class StorageHelper {
|
|||||||
remoteOnlyRecords.size());
|
remoteOnlyRecords.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final var listListPair = processKnownRecords(connection, remoteOnlyRecords);
|
||||||
|
final var unknownInserts = listListPair.first();
|
||||||
|
final var updatedStorageIds = listListPair.second();
|
||||||
|
final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds());
|
||||||
|
updatedStorageIds.forEach(oldUnregisteredLocalOnlyIds::remove);
|
||||||
if (!idDifference.localOnlyIds().isEmpty()) {
|
if (!idDifference.localOnlyIds().isEmpty()) {
|
||||||
final var updated = account.getRecipientStore()
|
final var updated = account.getRecipientStore()
|
||||||
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
||||||
idDifference.localOnlyIds());
|
oldUnregisteredLocalOnlyIds);
|
||||||
|
final var updatedStickers = account.getStickerStore()
|
||||||
|
.removeStorageIdsFromLocalOnlyDeletedStickerPacks(connection, oldUnregisteredLocalOnlyIds);
|
||||||
|
|
||||||
if (updated > 0) {
|
if (updated > 0 || updatedStickers > 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
|
"Found {} recipients and {} sticker packs that were deleted remotely but only marked deleted locally. Removed those from local store.",
|
||||||
updated);
|
updated,
|
||||||
|
updatedStickers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
|
|
||||||
final var unknownDeletes = idDifference.localOnlyIds()
|
final var unknownDeletes = idDifference.localOnlyIds()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
|
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
|
||||||
@ -274,7 +295,8 @@ public class StorageHelper {
|
|||||||
private boolean writeToStorage(
|
private boolean writeToStorage(
|
||||||
final StorageKey storageKey,
|
final StorageKey storageKey,
|
||||||
final SignalStorageManifest remoteManifest,
|
final SignalStorageManifest remoteManifest,
|
||||||
final boolean needsForcePush
|
final boolean needsForcePush,
|
||||||
|
final boolean fetchedRemoteManifest
|
||||||
) throws IOException, RetryLaterException {
|
) throws IOException, RetryLaterException {
|
||||||
final WriteOperationResult remoteWriteOperation;
|
final WriteOperationResult remoteWriteOperation;
|
||||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||||
@ -312,6 +334,19 @@ public class StorageHelper {
|
|||||||
|
|
||||||
if (remoteWriteOperation.isEmpty()) {
|
if (remoteWriteOperation.isEmpty()) {
|
||||||
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
||||||
|
storageSyncLoopDetector.onConverged();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var loopCheck = storageSyncLoopDetector.onWriteAttempt(remoteWriteOperation,
|
||||||
|
fetchedRemoteManifest,
|
||||||
|
false);
|
||||||
|
if (loopCheck instanceof StorageSyncLoopDetector.Decision.Denied denied) {
|
||||||
|
logger.warn(
|
||||||
|
"Skipping remote write, another device is likely undoing it. Cause: {}, level: {}. WriteOperationResult :: {}",
|
||||||
|
denied.cause(),
|
||||||
|
denied.level(),
|
||||||
|
remoteWriteOperation);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,11 +365,18 @@ public class StorageHelper {
|
|||||||
remoteWriteOperation.deletes());
|
remoteWriteOperation.deletes());
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case WriteStorageRecordsResult.ConflictError ignored -> {
|
case WriteStorageRecordsResult.ConflictError ignored -> {
|
||||||
|
storageSyncLoopDetector.onWriteFailed();
|
||||||
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||||
throw new RetryLaterException();
|
throw new RetryLaterException();
|
||||||
}
|
}
|
||||||
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
case WriteStorageRecordsResult.NetworkError networkError -> {
|
||||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
storageSyncLoopDetector.onWriteFailed();
|
||||||
|
throw networkError.getException();
|
||||||
|
}
|
||||||
|
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> {
|
||||||
|
storageSyncLoopDetector.onWriteFailed();
|
||||||
|
throw statusCodeError.getException();
|
||||||
|
}
|
||||||
case WriteStorageRecordsResult.Success ignored -> {
|
case WriteStorageRecordsResult.Success ignored -> {
|
||||||
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
||||||
storeManifestLocally(remoteWriteOperation.manifest());
|
storeManifestLocally(remoteWriteOperation.manifest());
|
||||||
@ -360,6 +402,7 @@ public class StorageHelper {
|
|||||||
final Map<RecipientId, StorageId> newContactStorageIds;
|
final Map<RecipientId, StorageId> newContactStorageIds;
|
||||||
final Map<GroupIdV1, StorageId> newGroupV1StorageIds;
|
final Map<GroupIdV1, StorageId> newGroupV1StorageIds;
|
||||||
final Map<GroupIdV2, StorageId> newGroupV2StorageIds;
|
final Map<GroupIdV2, StorageId> newGroupV2StorageIds;
|
||||||
|
final Map<StickerPackId, StorageId> newStickerPackStorageIds;
|
||||||
|
|
||||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||||
connection.setAutoCommit(false);
|
connection.setAutoCommit(false);
|
||||||
@ -406,6 +449,19 @@ public class StorageHelper {
|
|||||||
new StorageRecord.Builder().groupV2(record).build()));
|
new StorageRecord.Builder().groupV2(record).build()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final var stickerPacks = account.getStickerStore()
|
||||||
|
.getStickerPacks(connection)
|
||||||
|
.stream()
|
||||||
|
.filter(pack -> pack.storageId() != null)
|
||||||
|
.toList();
|
||||||
|
newStickerPackStorageIds = generateStickerPackStorageIds(stickerPacks);
|
||||||
|
for (final var stickerPack : stickerPacks) {
|
||||||
|
final var storageId = newStickerPackStorageIds.get(stickerPack.packId());
|
||||||
|
final var record = StorageSyncModels.localToRemoteRecord(stickerPack);
|
||||||
|
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||||
|
new StorageRecord.Builder().stickerPack(record).build()));
|
||||||
|
}
|
||||||
|
|
||||||
connection.commit();
|
connection.commit();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException("Failed to sync remote storage", e);
|
throw new RuntimeException("Failed to sync remote storage", e);
|
||||||
@ -456,6 +512,7 @@ public class StorageHelper {
|
|||||||
connection.setAutoCommit(false);
|
connection.setAutoCommit(false);
|
||||||
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
|
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
|
||||||
account.getGroupStore().updateStorageIds(connection, newGroupV1StorageIds, newGroupV2StorageIds);
|
account.getGroupStore().updateStorageIds(connection, newGroupV1StorageIds, newGroupV2StorageIds);
|
||||||
|
account.getStickerStore().updateStorageIds(connection, newStickerPackStorageIds);
|
||||||
|
|
||||||
// delete all unknown storage ids
|
// delete all unknown storage ids
|
||||||
account.getUnknownStorageIdStore().deleteAllUnknownStorageIds(connection);
|
account.getUnknownStorageIdStore().deleteAllUnknownStorageIds(connection);
|
||||||
@ -479,13 +536,21 @@ public class StorageHelper {
|
|||||||
private Map<GroupIdV1, StorageId> generateGroupV1StorageIds(List<GroupIdV1> groupIds) {
|
private Map<GroupIdV1, StorageId> generateGroupV1StorageIds(List<GroupIdV1> groupIds) {
|
||||||
return groupIds.stream()
|
return groupIds.stream()
|
||||||
.collect(Collectors.toMap(recipientId -> recipientId,
|
.collect(Collectors.toMap(recipientId -> recipientId,
|
||||||
recipientId -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
|
_ -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<GroupIdV2, StorageId> generateGroupV2StorageIds(List<GroupIdV2> groupIds) {
|
private Map<GroupIdV2, StorageId> generateGroupV2StorageIds(List<GroupIdV2> groupIds) {
|
||||||
return groupIds.stream()
|
return groupIds.stream()
|
||||||
.collect(Collectors.toMap(recipientId -> recipientId,
|
.collect(Collectors.toMap(recipientId -> recipientId,
|
||||||
recipientId -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
_ -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<StickerPackId, StorageId> generateStickerPackStorageIds(
|
||||||
|
final List<StickerPack> stickerPacks
|
||||||
|
) {
|
||||||
|
return stickerPacks.stream()
|
||||||
|
.collect(Collectors.toMap(stickerPack -> stickerPack.packId(),
|
||||||
|
_ -> StorageId.forStickerPack(KeyUtils.createRawStorageId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storeManifestLocally(
|
private void storeManifestLocally(
|
||||||
@ -503,7 +568,7 @@ public class StorageHelper {
|
|||||||
final var result = dependencies.getStorageServiceRepository()
|
final var result = dependencies.getStorageServiceRepository()
|
||||||
.readStorageRecords(storageKey, manifest.recordIkm, storageIds);
|
.readStorageRecords(storageKey, manifest.recordIkm, storageIds);
|
||||||
return switch (result) {
|
return switch (result) {
|
||||||
case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
|
case StorageServiceService.StorageRecordResult.DecryptionError decryptionError -> {
|
||||||
if (decryptionError.getException() instanceof InvalidKeyException) {
|
if (decryptionError.getException() instanceof InvalidKeyException) {
|
||||||
logger.warn("Failed to read storage records, ignoring.");
|
logger.warn("Failed to read storage records, ignoring.");
|
||||||
yield List.of();
|
yield List.of();
|
||||||
@ -513,11 +578,11 @@ public class StorageHelper {
|
|||||||
throw new IOException(decryptionError.getException());
|
throw new IOException(decryptionError.getException());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case StorageServiceRepository.StorageRecordResult.NetworkError networkError ->
|
case StorageServiceService.StorageRecordResult.NetworkError networkError ->
|
||||||
throw networkError.getException();
|
throw networkError.getException();
|
||||||
case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError ->
|
case StorageServiceService.StorageRecordResult.StatusCodeError statusCodeError ->
|
||||||
throw statusCodeError.getException();
|
throw statusCodeError.getException();
|
||||||
case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords();
|
case StorageServiceService.StorageRecordResult.Success success -> success.getRecords();
|
||||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -527,6 +592,7 @@ public class StorageHelper {
|
|||||||
storageIds.addAll(account.getUnknownStorageIdStore().getUnknownStorageIds(connection));
|
storageIds.addAll(account.getUnknownStorageIdStore().getUnknownStorageIds(connection));
|
||||||
storageIds.addAll(account.getGroupStore().getStorageIds(connection));
|
storageIds.addAll(account.getGroupStore().getStorageIds(connection));
|
||||||
storageIds.addAll(account.getRecipientStore().getStorageIds(connection));
|
storageIds.addAll(account.getRecipientStore().getStorageIds(connection));
|
||||||
|
storageIds.addAll(account.getStickerStore().getStorageIds(connection));
|
||||||
storageIds.add(account.getRecipientStore().getSelfStorageId(connection));
|
storageIds.add(account.getRecipientStore().getSelfStorageId(connection));
|
||||||
return storageIds;
|
return storageIds;
|
||||||
}
|
}
|
||||||
@ -575,6 +641,14 @@ public class StorageHelper {
|
|||||||
account.getUsernameLink());
|
account.getUsernameLink());
|
||||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
|
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
|
||||||
}
|
}
|
||||||
|
case ManifestRecord.Identifier.Type.STICKER_PACK -> {
|
||||||
|
final var stickerPack = account.getStickerStore().getStickerPack(connection, storageId);
|
||||||
|
if (stickerPack == null) {
|
||||||
|
throw new AssertionError("Missing local sticker pack model for storage id: " + storageId);
|
||||||
|
}
|
||||||
|
final var record = StorageSyncModels.localToRemoteRecord(stickerPack);
|
||||||
|
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().stickerPack(record).build());
|
||||||
|
}
|
||||||
case null, default -> {
|
case null, default -> {
|
||||||
throw new AssertionError("Got unknown local storage record type: " + storageId);
|
throw new AssertionError("Got unknown local storage record type: " + storageId);
|
||||||
}
|
}
|
||||||
@ -629,16 +703,25 @@ public class StorageHelper {
|
|||||||
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
|
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<StorageId> processKnownRecords(
|
private Pair<List<StorageId>, List<StorageId>> processKnownRecords(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
List<SignalStorageRecord> records
|
List<SignalStorageRecord> records
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var unknownRecords = new ArrayList<StorageId>();
|
final var unknownRecords = new ArrayList<StorageId>();
|
||||||
|
final var processedRecords = new ArrayList<StorageId>();
|
||||||
|
|
||||||
final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor());
|
final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor());
|
||||||
final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
|
|
||||||
final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection);
|
final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection);
|
||||||
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
|
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
|
||||||
|
final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
|
||||||
|
final var stickerPackRecordProcessor = new StickerPackRecordProcessor(account, connection);
|
||||||
|
|
||||||
|
final var contactRecords = records.stream()
|
||||||
|
.filter(record -> record.getProto().contact != null)
|
||||||
|
.map(record -> StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
||||||
|
record.getId()))
|
||||||
|
.toList();
|
||||||
|
contactRecordProcessor.prepare(contactRecords);
|
||||||
|
|
||||||
for (final var record : records) {
|
for (final var record : records) {
|
||||||
if (record.getProto().account != null) {
|
if (record.getProto().account != null) {
|
||||||
@ -657,12 +740,21 @@ public class StorageHelper {
|
|||||||
logger.debug("Reading record {} of type contact", record.getId());
|
logger.debug("Reading record {} of type contact", record.getId());
|
||||||
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
||||||
record.getId()));
|
record.getId()));
|
||||||
|
} else if (record.getProto().stickerPack != null) {
|
||||||
|
logger.debug("Reading record {} of type stickerPack", record.getId());
|
||||||
|
stickerPackRecordProcessor.process(StorageRecordConvertersKt.toSignalStickerPackRecord(record.getProto().stickerPack,
|
||||||
|
record.getId()));
|
||||||
} else {
|
} else {
|
||||||
unknownRecords.add(record.getId());
|
unknownRecords.add(record.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
processedRecords.addAll(accountRecordProcessor.getUpdatedStorageIds());
|
||||||
|
processedRecords.addAll(groupV1RecordProcessor.getUpdatedStorageIds());
|
||||||
|
processedRecords.addAll(groupV2RecordProcessor.getUpdatedStorageIds());
|
||||||
|
processedRecords.addAll(contactRecordProcessor.getUpdatedStorageIds());
|
||||||
|
processedRecords.addAll(stickerPackRecordProcessor.getUpdatedStorageIds());
|
||||||
|
|
||||||
return unknownRecords;
|
return new Pair<>(unknownRecords, processedRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import org.asamk.signal.manager.storage.stickers.StickerPack;
|
|||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
import org.asamk.signal.manager.util.MimeUtils;
|
import org.asamk.signal.manager.util.MimeUtils;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -36,8 +37,8 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
|
|||||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
import org.whispersystems.signalservice.internal.push.SyncMessage;
|
import org.whispersystems.signalservice.internal.push.SyncMessage;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@ -52,6 +53,8 @@ import java.util.Optional;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import okio.ByteString;
|
||||||
|
|
||||||
public class SyncHelper {
|
public class SyncHelper {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SyncHelper.class);
|
private static final Logger logger = LoggerFactory.getLogger(SyncHelper.class);
|
||||||
@ -86,7 +89,7 @@ public class SyncHelper {
|
|||||||
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST));
|
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendSyncReceiptMessage(ServiceId sender, SignalServiceReceiptMessage receiptMessage) {
|
public void sendSyncReceiptMessage(ServiceId.ACI sender, SignalServiceReceiptMessage receiptMessage) {
|
||||||
if (receiptMessage.isReadReceipt()) {
|
if (receiptMessage.isReadReceipt()) {
|
||||||
final var readMessages = receiptMessage.getTimestamps()
|
final var readMessages = receiptMessage.getTimestamps()
|
||||||
.stream()
|
.stream()
|
||||||
@ -112,7 +115,7 @@ public class SyncHelper {
|
|||||||
if (record instanceof GroupInfoV1 groupInfo) {
|
if (record instanceof GroupInfoV1 groupInfo) {
|
||||||
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
|
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
|
||||||
Optional.ofNullable(groupInfo.name),
|
Optional.ofNullable(groupInfo.name),
|
||||||
groupInfo.getMembers()
|
groupInfo.getMemberRecipientIds()
|
||||||
.stream()
|
.stream()
|
||||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -129,11 +132,14 @@ public class SyncHelper {
|
|||||||
|
|
||||||
if (groupsFile.exists() && groupsFile.length() > 0) {
|
if (groupsFile.exists() && groupsFile.length() > 0) {
|
||||||
try (var groupsFileStream = new FileInputStream(groupsFile)) {
|
try (var groupsFileStream = new FileInputStream(groupsFile)) {
|
||||||
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
|
final var streamDetails = new StreamDetails(groupsFileStream,
|
||||||
|
MimeUtils.OCTET_STREAM,
|
||||||
|
groupsFile.length());
|
||||||
|
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
||||||
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
||||||
.withStream(groupsFileStream)
|
.withStream(streamDetails.getStream())
|
||||||
.withContentType(MimeUtils.OCTET_STREAM)
|
.withContentType(streamDetails.getContentType())
|
||||||
.withLength(groupsFile.length())
|
.withLength(streamDetails.getLength())
|
||||||
.withResumableUploadSpec(uploadSpec)
|
.withResumableUploadSpec(uploadSpec)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -188,11 +194,14 @@ public class SyncHelper {
|
|||||||
|
|
||||||
if (contactsFile.exists() && contactsFile.length() > 0) {
|
if (contactsFile.exists() && contactsFile.length() > 0) {
|
||||||
try (var contactsFileStream = new FileInputStream(contactsFile)) {
|
try (var contactsFileStream = new FileInputStream(contactsFile)) {
|
||||||
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
|
final var streamDetails = new StreamDetails(contactsFileStream,
|
||||||
|
MimeUtils.OCTET_STREAM,
|
||||||
|
contactsFile.length());
|
||||||
|
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
||||||
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
||||||
.withStream(contactsFileStream)
|
.withStream(streamDetails.getStream())
|
||||||
.withContentType(MimeUtils.OCTET_STREAM)
|
.withContentType(streamDetails.getContentType())
|
||||||
.withLength(contactsFile.length())
|
.withLength(streamDetails.getLength())
|
||||||
.withResumableUploadSpec(uploadSpec)
|
.withResumableUploadSpec(uploadSpec)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -228,18 +237,19 @@ public class SyncHelper {
|
|||||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
||||||
if (address.aci().isPresent() || address.number().isPresent()) {
|
if (address.aci().isPresent() || address.number().isPresent()) {
|
||||||
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
||||||
address.number().orElse(null)));
|
address.number().orElse(null),
|
||||||
|
record.second().blockedAt()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var groupIds = new ArrayList<byte[]>();
|
var groups = new ArrayList<BlockedListMessage.Group>();
|
||||||
for (var record : account.getGroupStore().getGroups()) {
|
for (var record : account.getGroupStore().getGroups()) {
|
||||||
if (record.isBlocked()) {
|
if (record.isBlocked()) {
|
||||||
groupIds.add(record.getGroupId().serialize());
|
groups.add(new BlockedListMessage.Group(record.getGroupId().serialize(), record.getBlockedAt()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return context.getSendHelper()
|
return context.getSendHelper()
|
||||||
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
|
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groups)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendVerifiedMessage(
|
public SendMessageResult sendVerifiedMessage(
|
||||||
@ -256,7 +266,6 @@ public class SyncHelper {
|
|||||||
|
|
||||||
public SendMessageResult sendKeysMessage() {
|
public SendMessageResult sendKeysMessage() {
|
||||||
var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
|
var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
|
||||||
account.getOrCreatePinMasterKey(),
|
|
||||||
account.getOrCreateAccountEntropyPool(),
|
account.getOrCreateAccountEntropyPool(),
|
||||||
account.getOrCreateMediaRootBackupKey());
|
account.getOrCreateMediaRootBackupKey());
|
||||||
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
|
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
|
||||||
@ -291,7 +300,7 @@ public class SyncHelper {
|
|||||||
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
|
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleSyncDeviceGroups(final InputStream input) {
|
public void handleSyncDeviceGroups(final InputStream input, final boolean ignoreAvatars) {
|
||||||
final var s = new DeviceGroupsInputStream(input);
|
final var s = new DeviceGroupsInputStream(input);
|
||||||
DeviceGroup g;
|
DeviceGroup g;
|
||||||
while (true) {
|
while (true) {
|
||||||
@ -324,7 +333,7 @@ public class SyncHelper {
|
|||||||
syncGroup.color = g.getColor().get();
|
syncGroup.color = g.getColor().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g.getAvatar().isPresent()) {
|
if (g.getAvatar().isPresent() && !ignoreAvatars) {
|
||||||
context.getGroupHelper().downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
|
context.getGroupHelper().downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
|
||||||
}
|
}
|
||||||
syncGroup.archived = g.isArchived();
|
syncGroup.archived = g.isArchived();
|
||||||
@ -333,7 +342,7 @@ public class SyncHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleSyncDeviceContacts(final InputStream input) throws IOException {
|
public void handleSyncDeviceContacts(final InputStream input, final boolean ignoreAvatars) throws IOException {
|
||||||
final var s = new DeviceContactsInputStream(input);
|
final var s = new DeviceContactsInputStream(input);
|
||||||
DeviceContact c;
|
DeviceContact c;
|
||||||
while (true) {
|
while (true) {
|
||||||
@ -379,7 +388,11 @@ public class SyncHelper {
|
|||||||
account.getContactStore().storeContact(recipientId, builder.build());
|
account.getContactStore().storeContact(recipientId, builder.build());
|
||||||
|
|
||||||
if (c.getAvatar().isPresent()) {
|
if (c.getAvatar().isPresent()) {
|
||||||
storeContactAvatar(c.getAvatar().get(), address);
|
if (!ignoreAvatars) {
|
||||||
|
storeContactAvatar(c.getAvatar().get(), address);
|
||||||
|
} else {
|
||||||
|
IOUtils.discardStream(c.getAvatar().get().getInputStream());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -406,6 +419,11 @@ public class SyncHelper {
|
|||||||
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
|
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SendMessageResult sendDeviceNameChange(final int deviceId) {
|
||||||
|
final var deviceNameChange = new SyncMessage.DeviceNameChange(deviceId, ByteString.EMPTY);
|
||||||
|
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forDeviceNameChange(deviceNameChange));
|
||||||
|
}
|
||||||
|
|
||||||
private SendMessageResult requestSyncData(final SyncMessage.Request.Type type) {
|
private SendMessageResult requestSyncData(final SyncMessage.Request.Type type) {
|
||||||
var r = new SyncMessage.Request.Builder().type(type).build();
|
var r = new SyncMessage.Request.Builder().type(type).build();
|
||||||
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
|
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package org.asamk.signal.manager.internal;
|
|||||||
|
|
||||||
import org.asamk.signal.manager.helper.AccountFileUpdater;
|
import org.asamk.signal.manager.helper.AccountFileUpdater;
|
||||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
|
||||||
public class AccountFileUpdaterImpl implements AccountFileUpdater {
|
public class AccountFileUpdaterImpl implements AccountFileUpdater {
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ public class JobExecutor implements AutoCloseable {
|
|||||||
|
|
||||||
public JobExecutor(final Context context) {
|
public JobExecutor(final Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.executorService = Executors.newCachedThreadPool();
|
this.executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enqueueJob(Job job) {
|
public void enqueueJob(Job job) {
|
||||||
|
|||||||
@ -19,6 +19,8 @@ package org.asamk.signal.manager.internal;
|
|||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||||
|
import org.asamk.signal.manager.api.CallInfo;
|
||||||
|
import org.asamk.signal.manager.api.CallOffer;
|
||||||
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
||||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.Configuration;
|
import org.asamk.signal.manager.api.Configuration;
|
||||||
@ -62,6 +64,7 @@ import org.asamk.signal.manager.api.StickerPackId;
|
|||||||
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
||||||
import org.asamk.signal.manager.api.StickerPackUrl;
|
import org.asamk.signal.manager.api.StickerPackUrl;
|
||||||
import org.asamk.signal.manager.api.TextStyle;
|
import org.asamk.signal.manager.api.TextStyle;
|
||||||
|
import org.asamk.signal.manager.api.TurnServer;
|
||||||
import org.asamk.signal.manager.api.TypingAction;
|
import org.asamk.signal.manager.api.TypingAction;
|
||||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||||
import org.asamk.signal.manager.api.UpdateGroup;
|
import org.asamk.signal.manager.api.UpdateGroup;
|
||||||
@ -80,6 +83,7 @@ import org.asamk.signal.manager.storage.AttachmentStore;
|
|||||||
import org.asamk.signal.manager.storage.AvatarStore;
|
import org.asamk.signal.manager.storage.AvatarStore;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||||
|
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||||
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
@ -91,27 +95,36 @@ import org.asamk.signal.manager.util.KeyUtils;
|
|||||||
import org.asamk.signal.manager.util.MimeUtils;
|
import org.asamk.signal.manager.util.MimeUtils;
|
||||||
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
||||||
import org.asamk.signal.manager.util.StickerUtils;
|
import org.asamk.signal.manager.util.StickerUtils;
|
||||||
import org.signal.core.util.Base64;
|
import org.signal.core.models.ServiceId;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
|
import org.signal.core.util.Hex;
|
||||||
|
import org.signal.core.util.crypto.DeviceName;
|
||||||
|
import org.signal.core.util.crypto.DeviceNameCipher;
|
||||||
|
import org.signal.libsignal.net.LinkedDevice;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
|
import org.signal.libsignal.protocol.NoSessionException;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
|
||||||
import org.whispersystems.signalservice.internal.util.Util;
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
@ -147,6 +160,7 @@ import okio.Utf8;
|
|||||||
|
|
||||||
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
|
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
|
||||||
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
|
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
|
||||||
|
|
||||||
public class ManagerImpl implements Manager {
|
public class ManagerImpl implements Manager {
|
||||||
@ -157,7 +171,7 @@ public class ManagerImpl implements Manager {
|
|||||||
private final SignalDependencies dependencies;
|
private final SignalDependencies dependencies;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
|
||||||
private Thread receiveThread;
|
private Thread receiveThread;
|
||||||
private boolean isReceivingSynchronous;
|
private boolean isReceivingSynchronous;
|
||||||
@ -182,6 +196,7 @@ public class ManagerImpl implements Manager {
|
|||||||
userAgent,
|
userAgent,
|
||||||
account.getCredentialsProvider(),
|
account.getCredentialsProvider(),
|
||||||
account.getSignalServiceDataStore(),
|
account.getSignalServiceDataStore(),
|
||||||
|
account.getDeviceId(),
|
||||||
executor,
|
executor,
|
||||||
sessionLock);
|
sessionLock);
|
||||||
final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
|
final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
|
||||||
@ -235,6 +250,11 @@ public class ManagerImpl implements Manager {
|
|||||||
return account.getNumber();
|
return account.getNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSelfACI() {
|
||||||
|
return account.getAci().toString();
|
||||||
|
}
|
||||||
|
|
||||||
public void checkAccountState() throws IOException {
|
public void checkAccountState() throws IOException {
|
||||||
context.getAccountHelper().checkAccountState();
|
context.getAccountHelper().checkAccountState();
|
||||||
final var lastRecipientsRefresh = account.getLastRecipientsRefresh();
|
final var lastRecipientsRefresh = account.getLastRecipientsRefresh();
|
||||||
@ -270,7 +290,7 @@ public class ManagerImpl implements Manager {
|
|||||||
registeredUsers = context.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet);
|
registeredUsers = context.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet);
|
||||||
} catch (CdsiResourceExhaustedException e) {
|
} catch (CdsiResourceExhaustedException e) {
|
||||||
logger.debug("CDSI resource exhausted: {}", e.getMessage());
|
logger.debug("CDSI resource exhausted: {}", e.getMessage());
|
||||||
throw new RateLimitException(System.currentTimeMillis() + e.getRetryAfterSeconds() * 1000L);
|
throw new RateLimitException(e.getRetryAfterSeconds() * 1000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
|
return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
|
||||||
@ -280,7 +300,7 @@ public class ManagerImpl implements Manager {
|
|||||||
final var profile = serviceId == null
|
final var profile = serviceId == null
|
||||||
? null
|
? null
|
||||||
: context.getProfileHelper()
|
: context.getProfileHelper()
|
||||||
.getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
|
.getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
|
||||||
return new UserStatus(number.isEmpty() ? null : number,
|
return new UserStatus(number.isEmpty() ? null : number,
|
||||||
serviceId == null ? null : serviceId.getRawUuid(),
|
serviceId == null ? null : serviceId.getRawUuid(),
|
||||||
profile != null
|
profile != null
|
||||||
@ -307,7 +327,7 @@ public class ManagerImpl implements Manager {
|
|||||||
final var profile = serviceId == null
|
final var profile = serviceId == null
|
||||||
? null
|
? null
|
||||||
: context.getProfileHelper()
|
: context.getProfileHelper()
|
||||||
.getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
|
.getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
|
||||||
return new UsernameStatus(username,
|
return new UsernameStatus(username,
|
||||||
serviceId == null ? null : serviceId.getRawUuid(),
|
serviceId == null ? null : serviceId.getRawUuid(),
|
||||||
profile != null
|
profile != null
|
||||||
@ -402,10 +422,8 @@ public class ManagerImpl implements Manager {
|
|||||||
} else {
|
} else {
|
||||||
context.getAccountHelper().reserveUsernameFromNickname(username);
|
context.getAccountHelper().reserveUsernameFromNickname(username);
|
||||||
}
|
}
|
||||||
} catch (UsernameMalformedException e) {
|
} catch (NonSuccessfulResponseCodeException e) {
|
||||||
throw new InvalidUsernameException("Username is malformed", e);
|
throw new InvalidUsernameException("Username is malformed or already taken", e);
|
||||||
} catch (UsernameTakenException e) {
|
|
||||||
throw new InvalidUsernameException("Username is already registered", e);
|
|
||||||
} catch (BaseUsernameException e) {
|
} catch (BaseUsernameException e) {
|
||||||
throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
|
throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
|
||||||
}
|
}
|
||||||
@ -466,28 +484,46 @@ public class ManagerImpl implements Manager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Device> getLinkedDevices() throws IOException {
|
public List<Device> getLinkedDevices() throws IOException {
|
||||||
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
final List<LinkedDevice> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||||
|
.getDevices(cont));
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
var identityKey = account.getAciIdentityKeyPair();
|
||||||
return devices.stream().map(d -> {
|
return devices.stream().map(d -> {
|
||||||
String deviceName = d.getName();
|
String deviceName = null;
|
||||||
if (deviceName != null) {
|
if (d.getEncryptedName() != null && d.getEncryptedName().length > 0) {
|
||||||
try {
|
try {
|
||||||
deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
|
deviceName = new String(DeviceNameCipher.decryptDeviceName(DeviceName.ADAPTER.decode(d.getEncryptedName()),
|
||||||
} catch (IOException e) {
|
identityKey), StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
logger.debug("Failed to decrypt device name, maybe plain text?", e);
|
logger.debug("Failed to decrypt device name, maybe plain text?", e);
|
||||||
|
deviceName = new String(d.getEncryptedName(), StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final var createdAt = getPlaintextCreatedAt(d);
|
final var createdAt = getPlaintextCreatedAt(d);
|
||||||
return new Device(d.getId(),
|
return new Device(d.getId(),
|
||||||
deviceName,
|
deviceName,
|
||||||
createdAt == null ? 0 : createdAt,
|
createdAt == null ? 0 : createdAt,
|
||||||
d.getLastSeen(),
|
d.getLastSeen().toEpochMilli(),
|
||||||
d.getId() == account.getDeviceId());
|
d.getId() == account.getDeviceId());
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getPlaintextCreatedAt(DeviceInfo d) {
|
@Override
|
||||||
|
public void updateLinkedDevice(
|
||||||
|
final int deviceId,
|
||||||
|
final String name
|
||||||
|
) throws IOException, NotPrimaryDeviceException {
|
||||||
|
if (deviceId == account.getDeviceId()) {
|
||||||
|
context.getAccountHelper().setDeviceName(name);
|
||||||
|
} else {
|
||||||
|
if (!account.isPrimaryDevice()) {
|
||||||
|
throw new NotPrimaryDeviceException();
|
||||||
|
}
|
||||||
|
context.getAccountHelper().setDeviceName(deviceId, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getPlaintextCreatedAt(LinkedDevice d) {
|
||||||
final var DECRYPTION_INFO = "deviceCreatedAt";
|
final var DECRYPTION_INFO = "deviceCreatedAt";
|
||||||
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
try {
|
try {
|
||||||
@ -495,8 +531,9 @@ public class ManagerImpl implements Manager {
|
|||||||
var associatedData = new ByteArrayOutputStream();
|
var associatedData = new ByteArrayOutputStream();
|
||||||
associatedData.write(d.getId());
|
associatedData.write(d.getId());
|
||||||
associatedData.write(ByteBuffer.allocate(4).putInt(d.getRegistrationId()).array());
|
associatedData.write(ByteBuffer.allocate(4).putInt(d.getRegistrationId()).array());
|
||||||
var createdAtPlaintext = identityKey.open(Base64.decode(d.getCreatedAtCiphertext()
|
var createdAtPlaintext = identityKey.open(d.getCreatedAtCiphertext(),
|
||||||
.getBytes(StandardCharsets.UTF_8)), DECRYPTION_INFO, associatedData.toByteArray());
|
DECRYPTION_INFO,
|
||||||
|
associatedData.toByteArray());
|
||||||
return ByteBuffer.wrap(createdAtPlaintext).getLong();
|
return ByteBuffer.wrap(createdAtPlaintext).getLong();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed while reading the protobuf.", e);
|
logger.warn("Failed while reading the protobuf.", e);
|
||||||
@ -573,6 +610,11 @@ public class ManagerImpl implements Manager {
|
|||||||
context.getGroupHelper().deleteGroup(groupId);
|
context.getGroupHelper().deleteGroup(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendGroupMessageResults terminateGroup(GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
||||||
|
return context.getGroupHelper().terminateGroup(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
String name,
|
String name,
|
||||||
@ -618,7 +660,9 @@ public class ManagerImpl implements Manager {
|
|||||||
updateGroup.getEditDetailsPermission(),
|
updateGroup.getEditDetailsPermission(),
|
||||||
updateGroup.getAvatarFile(),
|
updateGroup.getAvatarFile(),
|
||||||
updateGroup.getExpirationTimer(),
|
updateGroup.getExpirationTimer(),
|
||||||
updateGroup.getIsAnnouncementGroup());
|
updateGroup.getIsAnnouncementGroup(),
|
||||||
|
updateGroup.getLabelEmoji(),
|
||||||
|
updateGroup.getLabelString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -651,14 +695,15 @@ public class ManagerImpl implements Manager {
|
|||||||
Set<RecipientIdentifier> recipients,
|
Set<RecipientIdentifier> recipients,
|
||||||
boolean notifySelf
|
boolean notifySelf
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||||
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty());
|
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SendMessageResults sendMessage(
|
private SendMessageResults sendMessage(
|
||||||
SignalServiceDataMessage.Builder messageBuilder,
|
SignalServiceDataMessage.Builder messageBuilder,
|
||||||
Set<RecipientIdentifier> recipients,
|
Set<RecipientIdentifier> recipients,
|
||||||
boolean notifySelf,
|
boolean notifySelf,
|
||||||
Optional<Long> editTargetTimestamp
|
Optional<Long> editTargetTimestamp,
|
||||||
|
boolean urgent
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||||
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||||
long timestamp = getNextMessageTimestamp();
|
long timestamp = getNextMessageTimestamp();
|
||||||
@ -670,22 +715,25 @@ public class ManagerImpl implements Manager {
|
|||||||
)) {
|
)) {
|
||||||
final var result = notifySelf
|
final var result = notifySelf
|
||||||
? context.getSendHelper()
|
? context.getSendHelper()
|
||||||
.sendMessage(messageBuilder, account.getSelfRecipientId(), editTargetTimestamp)
|
.sendMessage(messageBuilder,
|
||||||
|
account.getSelfRecipientId(),
|
||||||
|
editTargetTimestamp,
|
||||||
|
urgent)
|
||||||
: context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
|
: context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
|
||||||
results.put(recipient, List.of(toSendMessageResult(result)));
|
results.put(recipient, List.of(toSendMessageResult(result)));
|
||||||
} else if (recipient instanceof RecipientIdentifier.Single single) {
|
} else if (recipient instanceof RecipientIdentifier.Single single) {
|
||||||
try {
|
try {
|
||||||
final var recipientId = context.getRecipientHelper().resolveRecipient(single);
|
final var recipientId = context.getRecipientHelper().resolveRecipient(single);
|
||||||
final var result = context.getSendHelper()
|
final var result = context.getSendHelper()
|
||||||
.sendMessage(messageBuilder, recipientId, editTargetTimestamp);
|
.sendMessage(messageBuilder, recipientId, editTargetTimestamp, urgent);
|
||||||
results.put(recipient, List.of(toSendMessageResult(result)));
|
results.put(recipient, List.of(toSendMessageResult(result)));
|
||||||
} catch (UnregisteredRecipientException e) {
|
} catch (UnregisteredRecipientException e) {
|
||||||
results.put(recipient,
|
results.put(recipient,
|
||||||
List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
|
List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
|
||||||
}
|
}
|
||||||
} else if (recipient instanceof RecipientIdentifier.Group group) {
|
} else if (recipient instanceof RecipientIdentifier.Group(GroupId groupId)) {
|
||||||
final var result = context.getSendHelper()
|
final var result = context.getSendHelper()
|
||||||
.sendAsGroupMessage(messageBuilder, group.groupId(), notifySelf, editTargetTimestamp);
|
.sendAsGroupMessage(messageBuilder, groupId, notifySelf, editTargetTimestamp, urgent);
|
||||||
results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
|
results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -758,15 +806,20 @@ public class ManagerImpl implements Manager {
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
|
final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
|
||||||
final var result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
|
List<SendMessageResult> results;
|
||||||
|
if (receiptMessage.isDeliveryReceipt() || !Boolean.FALSE.equals(account.getConfigurationStore()
|
||||||
final var serviceId = account.getRecipientAddressResolver()
|
.getReadReceipts())) {
|
||||||
.resolveRecipientAddress(recipientId)
|
final var result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
|
||||||
.serviceId();
|
results = List.of(toSendMessageResult(result));
|
||||||
if (serviceId.isPresent()) {
|
} else {
|
||||||
context.getSyncHelper().sendSyncReceiptMessage(serviceId.get(), receiptMessage);
|
results = List.of();
|
||||||
}
|
}
|
||||||
return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result))));
|
|
||||||
|
final var aci = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).aci();
|
||||||
|
if (aci.isPresent()) {
|
||||||
|
context.getSyncHelper().sendSyncReceiptMessage(aci.get(), receiptMessage);
|
||||||
|
}
|
||||||
|
return new SendMessageResults(timestamp, Map.of(sender, results));
|
||||||
} catch (UnregisteredRecipientException e) {
|
} catch (UnregisteredRecipientException e) {
|
||||||
return new SendMessageResults(timestamp,
|
return new SendMessageResults(timestamp,
|
||||||
Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress()))));
|
Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress()))));
|
||||||
@ -786,7 +839,7 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
final var messageBuilder = SignalServiceDataMessage.newBuilder();
|
final var messageBuilder = SignalServiceDataMessage.newBuilder();
|
||||||
applyMessage(messageBuilder, message);
|
applyMessage(messageBuilder, message);
|
||||||
return sendMessage(messageBuilder, recipients, notifySelf);
|
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty(), message.urgent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -797,7 +850,99 @@ public class ManagerImpl implements Manager {
|
|||||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
|
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
|
||||||
final var messageBuilder = SignalServiceDataMessage.newBuilder();
|
final var messageBuilder = SignalServiceDataMessage.newBuilder();
|
||||||
applyMessage(messageBuilder, message);
|
applyMessage(messageBuilder, message);
|
||||||
return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp));
|
return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp), message.urgent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendMessageResults sendStory(
|
||||||
|
String attachment,
|
||||||
|
boolean allowsReplies,
|
||||||
|
Optional<GroupId> groupId
|
||||||
|
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException {
|
||||||
|
final var file = new File(attachment);
|
||||||
|
final var mimeType = MimeUtils.getFileMimeType(file);
|
||||||
|
if (mimeType.isEmpty() || (!mimeType.get().startsWith("image/") && !mimeType.get().startsWith("video/"))) {
|
||||||
|
throw new AttachmentInvalidException(attachment,
|
||||||
|
new IOException("Stories only support image and video attachments"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupId.isPresent()) {
|
||||||
|
return sendGroupStory(attachment, allowsReplies, groupId.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
final var recipients = account.getRecipientStore()
|
||||||
|
.getRecipients(true, Optional.of(false), Set.of(), Optional.empty());
|
||||||
|
final var recipientIds = recipients.stream()
|
||||||
|
.filter(r -> !r.getRecipientId().equals(account.getSelfRecipientId()))
|
||||||
|
.filter(r -> r.getContact() != null && !r.getContact().hideStory())
|
||||||
|
.map(r -> r.getRecipientId())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (recipientIds.isEmpty()) {
|
||||||
|
throw new IOException("No eligible contacts found for story delivery");
|
||||||
|
}
|
||||||
|
|
||||||
|
final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment);
|
||||||
|
final var storyMessage = SignalServiceStoryMessage.forFileAttachment(account.getProfileKey().serialize(),
|
||||||
|
null,
|
||||||
|
uploadedAttachment,
|
||||||
|
allowsReplies,
|
||||||
|
List.of());
|
||||||
|
final var timestamp = getNextMessageTimestamp();
|
||||||
|
|
||||||
|
final var sendResults = context.getSendHelper()
|
||||||
|
.sendStoryMessage(storyMessage, timestamp, recipientIds, allowsReplies);
|
||||||
|
|
||||||
|
final var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||||
|
for (final var sendResult : sendResults) {
|
||||||
|
final var result = toSendMessageResult(sendResult);
|
||||||
|
results.put(RecipientIdentifier.Single.fromAddress(result.address()), List.of(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SendMessageResults(timestamp, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResults sendGroupStory(
|
||||||
|
String attachment,
|
||||||
|
boolean allowsReplies,
|
||||||
|
GroupId groupId
|
||||||
|
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException {
|
||||||
|
final var groupInfo = context.getGroupHelper().getGroup(groupId);
|
||||||
|
if (groupInfo == null) {
|
||||||
|
throw new GroupNotFoundException(groupId);
|
||||||
|
}
|
||||||
|
if (!groupInfo.isMember(account.getSelfRecipientId())) {
|
||||||
|
throw new NotAGroupMemberException(groupId, groupInfo.getTitle());
|
||||||
|
}
|
||||||
|
if (!(groupInfo instanceof GroupInfoV2 groupInfoV2)) {
|
||||||
|
throw new IOException("Stories are only supported for V2 groups");
|
||||||
|
}
|
||||||
|
if (groupInfoV2.isTerminated()) {
|
||||||
|
// Other clients drop messages sent to a terminated group.
|
||||||
|
throw new IOException("Cannot send a story to a group that has been terminated");
|
||||||
|
}
|
||||||
|
|
||||||
|
final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment);
|
||||||
|
final var groupContext = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
|
||||||
|
.withRevision(groupInfoV2.getGroup() == null ? 0 : groupInfoV2.getGroup().revision)
|
||||||
|
.build();
|
||||||
|
final var storyMessage = SignalServiceStoryMessage.forFileAttachment(account.getProfileKey().serialize(),
|
||||||
|
groupContext,
|
||||||
|
uploadedAttachment,
|
||||||
|
allowsReplies,
|
||||||
|
List.of());
|
||||||
|
final var timestamp = getNextMessageTimestamp();
|
||||||
|
|
||||||
|
final var sendResults = context.getSendHelper()
|
||||||
|
.sendGroupStoryMessage(storyMessage, timestamp, groupInfoV2, allowsReplies);
|
||||||
|
|
||||||
|
final var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||||
|
for (final var sendResult : sendResults) {
|
||||||
|
final var result = toSendMessageResult(sendResult);
|
||||||
|
results.put(RecipientIdentifier.Single.fromAddress(result.address()), List.of(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SendMessageResults(timestamp, results);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyMessage(
|
private void applyMessage(
|
||||||
@ -811,10 +956,10 @@ public class ManagerImpl implements Manager {
|
|||||||
final var remainder = result.getSecond();
|
final var remainder = result.getSecond();
|
||||||
if (remainder != null) {
|
if (remainder != null) {
|
||||||
final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
|
final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
|
||||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
|
||||||
final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
|
final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
|
||||||
MimeUtils.LONG_TEXT,
|
MimeUtils.LONG_TEXT,
|
||||||
messageBytes.length);
|
messageBytes.length);
|
||||||
|
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
||||||
final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
uploadSpec);
|
uploadSpec);
|
||||||
@ -827,7 +972,8 @@ public class ManagerImpl implements Manager {
|
|||||||
messageBuilder.withBody(message.messageText());
|
messageBuilder.withBody(message.messageText());
|
||||||
}
|
}
|
||||||
if (!message.attachments().isEmpty()) {
|
if (!message.attachments().isEmpty()) {
|
||||||
final var uploadedAttachments = context.getAttachmentHelper().uploadAttachments(message.attachments());
|
final var uploadedAttachments = context.getAttachmentHelper()
|
||||||
|
.uploadAttachments(message.attachments(), message.voiceNote());
|
||||||
if (!additionalAttachments.isEmpty()) {
|
if (!additionalAttachments.isEmpty()) {
|
||||||
additionalAttachments.addAll(uploadedAttachments);
|
additionalAttachments.addAll(uploadedAttachments);
|
||||||
messageBuilder.withAttachments(additionalAttachments);
|
messageBuilder.withAttachments(additionalAttachments);
|
||||||
@ -881,7 +1027,7 @@ public class ManagerImpl implements Manager {
|
|||||||
if (streamDetails == null) {
|
if (streamDetails == null) {
|
||||||
throw new InvalidStickerException("Missing local sticker file");
|
throw new InvalidStickerException("Missing local sticker file");
|
||||||
}
|
}
|
||||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
||||||
final var stickerAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
final var stickerAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
uploadSpec);
|
uploadSpec);
|
||||||
@ -895,7 +1041,7 @@ public class ManagerImpl implements Manager {
|
|||||||
final var previews = new ArrayList<SignalServicePreview>(message.previews().size());
|
final var previews = new ArrayList<SignalServicePreview>(message.previews().size());
|
||||||
for (final var p : message.previews()) {
|
for (final var p : message.previews()) {
|
||||||
final var image = p.image().isPresent() ? context.getAttachmentHelper()
|
final var image = p.image().isPresent() ? context.getAttachmentHelper()
|
||||||
.uploadAttachment(p.image().get()) : null;
|
.uploadAttachment(p.image().get()) : null;
|
||||||
previews.add(new SignalServicePreview(p.url(),
|
previews.add(new SignalServicePreview(p.url(),
|
||||||
p.title(),
|
p.title(),
|
||||||
p.description(),
|
p.description(),
|
||||||
@ -933,12 +1079,10 @@ public class ManagerImpl implements Manager {
|
|||||||
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
|
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
|
||||||
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
|
||||||
for (final var recipient : recipients) {
|
for (final var recipient : recipients) {
|
||||||
if (recipient instanceof RecipientIdentifier.Uuid u) {
|
if (recipient instanceof RecipientIdentifier.Uuid(var uuid)) {
|
||||||
account.getMessageSendLogStore()
|
account.getMessageSendLogStore().deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(uuid));
|
||||||
.deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid()));
|
} else if (recipient instanceof RecipientIdentifier.Pni(var pni)) {
|
||||||
} else if (recipient instanceof RecipientIdentifier.Pni pni) {
|
account.getMessageSendLogStore().deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni));
|
||||||
account.getMessageSendLogStore()
|
|
||||||
.deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni.pni()));
|
|
||||||
} else if (recipient instanceof RecipientIdentifier.Single r) {
|
} else if (recipient instanceof RecipientIdentifier.Single r) {
|
||||||
try {
|
try {
|
||||||
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
|
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
|
||||||
@ -949,8 +1093,8 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
} catch (UnregisteredRecipientException ignored) {
|
} catch (UnregisteredRecipientException ignored) {
|
||||||
}
|
}
|
||||||
} else if (recipient instanceof RecipientIdentifier.Group r) {
|
} else if (recipient instanceof RecipientIdentifier.Group(var groupId)) {
|
||||||
account.getMessageSendLogStore().deleteEntryForGroup(targetSentTimestamp, r.groupId());
|
account.getMessageSendLogStore().deleteEntryForGroup(targetSentTimestamp, groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sendMessage(messageBuilder, recipients, false);
|
return sendMessage(messageBuilder, recipients, false);
|
||||||
@ -963,6 +1107,7 @@ public class ManagerImpl implements Manager {
|
|||||||
RecipientIdentifier.Single targetAuthor,
|
RecipientIdentifier.Single targetAuthor,
|
||||||
long targetSentTimestamp,
|
long targetSentTimestamp,
|
||||||
Set<RecipientIdentifier> recipients,
|
Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf,
|
||||||
final boolean isStory
|
final boolean isStory
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
||||||
var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
|
var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
|
||||||
@ -975,7 +1120,78 @@ public class ManagerImpl implements Manager {
|
|||||||
messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId,
|
messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId,
|
||||||
targetSentTimestamp));
|
targetSentTimestamp));
|
||||||
}
|
}
|
||||||
return sendMessage(messageBuilder, recipients, false);
|
return sendMessage(messageBuilder, recipients, notifySelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendMessageResults sendAdminDelete(
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier.Group> recipients,
|
||||||
|
final boolean notifySelf,
|
||||||
|
final boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
||||||
|
final var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
|
||||||
|
final var authorServiceId = context.getRecipientHelper()
|
||||||
|
.resolveSignalServiceAddress(targetAuthorRecipientId)
|
||||||
|
.getServiceId();
|
||||||
|
final var adminDelete = new SignalServiceDataMessage.AdminDelete(authorServiceId, targetSentTimestamp);
|
||||||
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().withAdminDelete(adminDelete);
|
||||||
|
if (isStory) {
|
||||||
|
messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId,
|
||||||
|
targetSentTimestamp));
|
||||||
|
}
|
||||||
|
return sendMessage(messageBuilder,
|
||||||
|
recipients.stream().map(r -> (RecipientIdentifier) r).collect(Collectors.toSet()),
|
||||||
|
notifySelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendMessageResults sendPinMessage(
|
||||||
|
int pinDuration,
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf,
|
||||||
|
final boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
||||||
|
final var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
|
||||||
|
final var authorServiceId = context.getRecipientHelper()
|
||||||
|
.resolveSignalServiceAddress(targetAuthorRecipientId)
|
||||||
|
.getServiceId();
|
||||||
|
final var duration = pinDuration >= 0 ? pinDuration : null;
|
||||||
|
final var forever = pinDuration < 0;
|
||||||
|
final var pinnedMessage = new SignalServiceDataMessage.PinnedMessage(authorServiceId,
|
||||||
|
targetSentTimestamp,
|
||||||
|
duration,
|
||||||
|
forever);
|
||||||
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().withPinnedMessage(pinnedMessage);
|
||||||
|
if (isStory) {
|
||||||
|
messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId,
|
||||||
|
targetSentTimestamp));
|
||||||
|
}
|
||||||
|
return sendMessage(messageBuilder, recipients, notifySelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendMessageResults sendUnpinMessage(
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf,
|
||||||
|
final boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
||||||
|
final var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
|
||||||
|
final var authorServiceId = context.getRecipientHelper()
|
||||||
|
.resolveSignalServiceAddress(targetAuthorRecipientId)
|
||||||
|
.getServiceId();
|
||||||
|
final var unpinnedMessage = new SignalServiceDataMessage.UnpinnedMessage(authorServiceId, targetSentTimestamp);
|
||||||
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().withUnpinnedMessage(unpinnedMessage);
|
||||||
|
if (isStory) {
|
||||||
|
messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId,
|
||||||
|
targetSentTimestamp));
|
||||||
|
}
|
||||||
|
return sendMessage(messageBuilder, recipients, notifySelf);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -995,30 +1211,26 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
|
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
|
||||||
var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
|
for (var recipient : recipients) {
|
||||||
|
final RecipientId recipientId;
|
||||||
try {
|
try {
|
||||||
return sendMessage(messageBuilder,
|
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||||
recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()),
|
} catch (UnregisteredRecipientException e) {
|
||||||
false);
|
continue;
|
||||||
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
|
}
|
||||||
throw new AssertionError(e);
|
final var recipientAddress = context.getAccount()
|
||||||
} finally {
|
.getRecipientAddressResolver()
|
||||||
for (var recipient : recipients) {
|
.resolveRecipientAddress(recipientId);
|
||||||
final RecipientId recipientId;
|
final var aciSessionStore = account.getAccountData(ServiceIdType.ACI).getSessionStore();
|
||||||
try {
|
final var pniSessionStore = account.getAccountData(ServiceIdType.PNI).getSessionStore();
|
||||||
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
if (recipientAddress.aci().isPresent()) {
|
||||||
} catch (UnregisteredRecipientException e) {
|
aciSessionStore.archiveSessions(recipientAddress.aci().get());
|
||||||
continue;
|
pniSessionStore.archiveSessions(recipientAddress.aci().get());
|
||||||
}
|
}
|
||||||
final var serviceId = context.getAccount()
|
if (recipientAddress.pni().isPresent()) {
|
||||||
.getRecipientAddressResolver()
|
aciSessionStore.archiveSessions(recipientAddress.pni().get());
|
||||||
.resolveRecipientAddress(recipientId)
|
pniSessionStore.archiveSessions(recipientAddress.pni().get());
|
||||||
.serviceId();
|
|
||||||
if (serviceId.isPresent()) {
|
|
||||||
account.getAccountData(ServiceIdType.ACI).getSessionStore().deleteAllSessions(serviceId.get());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1051,14 +1263,59 @@ public class ManagerImpl implements Manager {
|
|||||||
results.put(recipient,
|
results.put(recipient,
|
||||||
List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
|
List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
|
||||||
}
|
}
|
||||||
} else if (recipient instanceof RecipientIdentifier.Group group) {
|
} else if (recipient instanceof RecipientIdentifier.Group(GroupId groupId)) {
|
||||||
final var result = context.getSyncHelper().sendMessageRequestResponse(type, group.groupId());
|
final var result = context.getSyncHelper().sendMessageRequestResponse(type, groupId);
|
||||||
results.put(recipient, List.of(toSendMessageResult(result)));
|
results.put(recipient, List.of(toSendMessageResult(result)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new SendMessageResults(0, results);
|
return new SendMessageResults(0, results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendMessageResults sendPollCreateMessage(
|
||||||
|
final String question,
|
||||||
|
final boolean allowMultiple,
|
||||||
|
final List<String> options,
|
||||||
|
final Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||||
|
final var pollCreate = new SignalServiceDataMessage.PollCreate(question, allowMultiple, options);
|
||||||
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().withPollCreate(pollCreate);
|
||||||
|
return sendMessage(messageBuilder, recipients, notifySelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public 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 {
|
||||||
|
final var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
|
||||||
|
final var authorServiceId = context.getRecipientHelper()
|
||||||
|
.resolveSignalServiceAddress(targetAuthorRecipientId)
|
||||||
|
.getServiceId();
|
||||||
|
final var pollVote = new SignalServiceDataMessage.PollVote(authorServiceId,
|
||||||
|
targetSentTimestamp,
|
||||||
|
optionIndexes,
|
||||||
|
voteCount);
|
||||||
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().withPollVote(pollVote);
|
||||||
|
return sendMessage(messageBuilder, recipients, notifySelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendMessageResults sendPollTerminateMessage(
|
||||||
|
final long targetSentTimestamp,
|
||||||
|
final Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||||
|
final var pollTerminate = new SignalServiceDataMessage.PollTerminate(targetSentTimestamp);
|
||||||
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().withPollTerminate(pollTerminate);
|
||||||
|
return sendMessage(messageBuilder, recipients, notifySelf);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hideRecipient(final RecipientIdentifier.Single recipient) {
|
public void hideRecipient(final RecipientIdentifier.Single recipient) {
|
||||||
final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
|
final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
|
||||||
@ -1094,10 +1351,7 @@ public class ManagerImpl implements Manager {
|
|||||||
final String nickGivenName,
|
final String nickGivenName,
|
||||||
final String nickFamilyName,
|
final String nickFamilyName,
|
||||||
final String note
|
final String note
|
||||||
) throws NotPrimaryDeviceException, UnregisteredRecipientException {
|
) throws UnregisteredRecipientException {
|
||||||
if (!account.isPrimaryDevice()) {
|
|
||||||
throw new NotPrimaryDeviceException();
|
|
||||||
}
|
|
||||||
context.getContactHelper()
|
context.getContactHelper()
|
||||||
.setContactName(context.getRecipientHelper().resolveRecipient(recipient),
|
.setContactName(context.getRecipientHelper().resolveRecipient(recipient),
|
||||||
givenName,
|
givenName,
|
||||||
@ -1423,7 +1677,21 @@ public class ManagerImpl implements Manager {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
// refresh profiles of explicitly given recipients
|
// refresh profiles of explicitly given recipients
|
||||||
context.getProfileHelper().refreshRecipientProfiles(recipientIds);
|
if (recipientIds.isEmpty()) {
|
||||||
|
final var rIds = account.getRecipientStore()
|
||||||
|
.getRecipients(onlyContacts, blocked, recipientIds, name)
|
||||||
|
.stream()
|
||||||
|
.filter(r -> r.isRegistered())
|
||||||
|
.map(r -> r.getRecipientId())
|
||||||
|
.toList();
|
||||||
|
try {
|
||||||
|
context.getProfileHelper().getRecipientProfiles(rIds);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to refresh profiles for recipients", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.getProfileHelper().refreshRecipientProfiles(recipientIds);
|
||||||
|
}
|
||||||
return account.getRecipientStore()
|
return account.getRecipientStore()
|
||||||
.getRecipients(onlyContacts, blocked, recipientIds, name)
|
.getRecipients(onlyContacts, blocked, recipientIds, name)
|
||||||
.stream()
|
.stream()
|
||||||
@ -1447,8 +1715,15 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final var contact = account.getContactStore().getContact(recipientId);
|
final var contact = account.getContactStore().getContact(recipientId);
|
||||||
if (contact != null && !Util.isEmpty(contact.getName())) {
|
if (contact != null) {
|
||||||
return contact.getName();
|
final var nickname = contact.getDisplayNickname();
|
||||||
|
if (!Util.isEmpty(nickname)) {
|
||||||
|
return nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Util.isEmpty(contact.getName())) {
|
||||||
|
return contact.getName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
|
final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
|
||||||
@ -1560,6 +1835,16 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCallEventListener(final CallEventListener listener) {
|
||||||
|
context.getCallManager().addCallEventListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeCallEventListener(final CallEventListener listener) {
|
||||||
|
context.getCallManager().removeCallEventListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream retrieveAttachment(final String id) throws IOException {
|
public InputStream retrieveAttachment(final String id) throws IOException {
|
||||||
return context.getAttachmentHelper().retrieveAttachment(id).getStream();
|
return context.getAttachmentHelper().retrieveAttachment(id).getStream();
|
||||||
@ -1617,6 +1902,142 @@ public class ManagerImpl implements Manager {
|
|||||||
return streamDetails.getStream();
|
return streamDetails.getStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Voice call methods ---
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CallInfo startCall(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
|
||||||
|
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||||
|
return context.getCallManager().startOutgoingCall(recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CallInfo acceptCall(final long callId) throws IOException {
|
||||||
|
return context.getCallManager().acceptIncomingCall(callId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hangupCall(final long callId) throws IOException {
|
||||||
|
context.getCallManager().hangupCall(callId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendMessageResult rejectCall(final long callId) throws IOException {
|
||||||
|
final var result = context.getCallManager().rejectCall(callId);
|
||||||
|
return toSendMessageResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CallInfo> listActiveCalls() {
|
||||||
|
return context.getCallManager().listActiveCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendCallOffer(
|
||||||
|
final RecipientIdentifier.Single recipient,
|
||||||
|
final CallOffer offer
|
||||||
|
) throws IOException, UnregisteredRecipientException {
|
||||||
|
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||||
|
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||||
|
var offerMessage = new OfferMessage(offer.callId(),
|
||||||
|
offer.type() == CallOffer.Type.VIDEO ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL,
|
||||||
|
offer.opaque());
|
||||||
|
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, null);
|
||||||
|
try {
|
||||||
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
|
} catch (UntrustedIdentityException e) {
|
||||||
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
|
} catch (NoSessionException e) {
|
||||||
|
throw new IOException("No session for call recipient", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendCallAnswer(
|
||||||
|
final RecipientIdentifier.Single recipient,
|
||||||
|
final long callId,
|
||||||
|
final byte[] answerOpaque
|
||||||
|
) throws IOException, UnregisteredRecipientException {
|
||||||
|
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||||
|
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||||
|
var answerMessage = new AnswerMessage(callId, answerOpaque);
|
||||||
|
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, null);
|
||||||
|
try {
|
||||||
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
|
} catch (UntrustedIdentityException e) {
|
||||||
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
|
} catch (NoSessionException e) {
|
||||||
|
throw new IOException("No session for call recipient", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendIceUpdate(
|
||||||
|
final RecipientIdentifier.Single recipient,
|
||||||
|
final long callId,
|
||||||
|
final List<byte[]> iceCandidates
|
||||||
|
) throws IOException, UnregisteredRecipientException {
|
||||||
|
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||||
|
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||||
|
var iceUpdates = iceCandidates.stream().map(opaque -> new IceUpdateMessage(callId, opaque)).toList();
|
||||||
|
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, null);
|
||||||
|
try {
|
||||||
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
|
} catch (UntrustedIdentityException e) {
|
||||||
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
|
} catch (NoSessionException e) {
|
||||||
|
throw new IOException("No session for call recipient", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendHangup(
|
||||||
|
final RecipientIdentifier.Single recipient,
|
||||||
|
final long callId,
|
||||||
|
final MessageEnvelope.Call.Hangup.Type type
|
||||||
|
) throws IOException, UnregisteredRecipientException {
|
||||||
|
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||||
|
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||||
|
var hangupType = switch (type) {
|
||||||
|
case NORMAL -> HangupMessage.Type.NORMAL;
|
||||||
|
case ACCEPTED -> HangupMessage.Type.ACCEPTED;
|
||||||
|
case DECLINED -> HangupMessage.Type.DECLINED;
|
||||||
|
case BUSY -> HangupMessage.Type.BUSY;
|
||||||
|
case NEED_PERMISSION -> HangupMessage.Type.NEED_PERMISSION;
|
||||||
|
};
|
||||||
|
var hangupMessage = new HangupMessage(callId, hangupType, 0);
|
||||||
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
||||||
|
try {
|
||||||
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
|
} catch (UntrustedIdentityException e) {
|
||||||
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
|
} catch (NoSessionException e) {
|
||||||
|
throw new IOException("No session for call recipient", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendBusy(
|
||||||
|
final RecipientIdentifier.Single recipient,
|
||||||
|
final long callId
|
||||||
|
) throws IOException, UnregisteredRecipientException {
|
||||||
|
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||||
|
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||||
|
var busyMessage = new BusyMessage(callId);
|
||||||
|
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, null);
|
||||||
|
try {
|
||||||
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
|
} catch (UntrustedIdentityException e) {
|
||||||
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
|
} catch (NoSessionException e) {
|
||||||
|
throw new IOException("No session for call recipient", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TurnServer> getTurnServerInfo() throws IOException {
|
||||||
|
return context.getCallManager().getTurnServers();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
Thread thread;
|
Thread thread;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import org.asamk.signal.manager.RegistrationManager;
|
|||||||
import org.asamk.signal.manager.SignalAccountFiles;
|
import org.asamk.signal.manager.SignalAccountFiles;
|
||||||
import org.asamk.signal.manager.api.AccountCheckException;
|
import org.asamk.signal.manager.api.AccountCheckException;
|
||||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
import org.asamk.signal.manager.api.NotRegisteredException;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -95,23 +96,47 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Manager getManager(final String number) {
|
public Manager getManager(final String identifier) {
|
||||||
synchronized (managers) {
|
synchronized (managers) {
|
||||||
final var manager = managers.stream()
|
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
||||||
.filter(m -> m.getSelfNumber().equals(number))
|
// Check if UUID corresponds to an already-loaded manager
|
||||||
.findFirst()
|
final var existing = managers.stream()
|
||||||
.orElse(null);
|
.filter(m -> m.getSelfACI().equals(identifier))
|
||||||
if (manager != null) {
|
.findFirst()
|
||||||
return manager;
|
.orElse(null);
|
||||||
}
|
if (existing != null) {
|
||||||
try {
|
logger.debug("Found already loaded manager for ACI: {}", identifier);
|
||||||
final var newManager = signalAccountFiles.initManager(number);
|
return existing;
|
||||||
managers.add(newManager);
|
}
|
||||||
return newManager;
|
// Load by ACI
|
||||||
} catch (IOException | NotRegisteredException | AccountCheckException e) {
|
try {
|
||||||
logger.warn("Failed to load new manager", e);
|
final var newManager = signalAccountFiles.initManagerByAci(identifier);
|
||||||
return null;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,34 +19,57 @@ package org.asamk.signal.manager.internal;
|
|||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.asamk.signal.manager.ProvisioningManager;
|
import org.asamk.signal.manager.ProvisioningManager;
|
||||||
import org.asamk.signal.manager.Settings;
|
import org.asamk.signal.manager.Settings;
|
||||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
|
||||||
import org.asamk.signal.manager.api.UserAlreadyExistsException;
|
import org.asamk.signal.manager.api.UserAlreadyExistsException;
|
||||||
import org.asamk.signal.manager.config.ServiceConfig;
|
import org.asamk.signal.manager.config.ServiceConfig;
|
||||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
|
import org.signal.core.models.AccountEntropyPool;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
|
import org.signal.core.models.backup.MediaRootBackupKey;
|
||||||
|
import org.signal.core.util.crypto.DeviceNameCipher;
|
||||||
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
|
||||||
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
|
import org.whispersystems.signalservice.api.account.DeviceAttributes;
|
||||||
|
import org.whispersystems.signalservice.api.provisioning.ProvisioningSocket;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
|
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher;
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
import org.whispersystems.signalservice.internal.push.ProvisionMessage;
|
||||||
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
|
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
|
||||||
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
|
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.nio.channels.OverlappingFileLockException;
|
import java.nio.channels.OverlappingFileLockException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
import kotlin.ResultKt;
|
||||||
|
import kotlin.Unit;
|
||||||
|
import kotlin.coroutines.Continuation;
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext;
|
||||||
|
import kotlin.coroutines.intrinsics.IntrinsicsKt;
|
||||||
|
import kotlin.jvm.functions.Function3;
|
||||||
|
import kotlinx.coroutines.BuildersKt;
|
||||||
|
import kotlinx.coroutines.CoroutineScope;
|
||||||
|
|
||||||
public class ProvisioningManagerImpl implements ProvisioningManager {
|
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
|
|
||||||
|
public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
|
private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
|
||||||
|
|
||||||
@ -56,9 +79,10 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||||||
private final Consumer<Manager> newManagerListener;
|
private final Consumer<Manager> newManagerListener;
|
||||||
private final AccountsStore accountsStore;
|
private final AccountsStore accountsStore;
|
||||||
|
|
||||||
private final ProvisioningApi provisioningApi;
|
|
||||||
private final IdentityKeyPair tempIdentityKey;
|
|
||||||
private final String password;
|
private final String password;
|
||||||
|
private final CompletableFuture<String> urlFuture = new CompletableFuture<>();
|
||||||
|
private final CompletableFuture<SecondaryProvisioningCipher.ProvisioningDecryptResult<ProvisionMessage>> messageFuture = new CompletableFuture<>();
|
||||||
|
private final Closeable socketHandle;
|
||||||
|
|
||||||
public ProvisioningManagerImpl(
|
public ProvisioningManagerImpl(
|
||||||
PathConfig pathConfig,
|
PathConfig pathConfig,
|
||||||
@ -73,35 +97,58 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||||||
this.newManagerListener = newManagerListener;
|
this.newManagerListener = newManagerListener;
|
||||||
this.accountsStore = accountsStore;
|
this.accountsStore = accountsStore;
|
||||||
|
|
||||||
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
final IdentityKeyPair tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||||
password = KeyUtils.createPassword();
|
password = KeyUtils.createPassword();
|
||||||
final var credentialsProvider = new DynamicCredentialsProvider(null,
|
|
||||||
null,
|
socketHandle = ProvisioningSocket.Companion.start(new ProvisioningSocket.Mode.Link(false),
|
||||||
null,
|
tempIdentityKey,
|
||||||
password,
|
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
SignalServiceAddress.DEFAULT_DEVICE_ID);
|
(id, t) -> {
|
||||||
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
urlFuture.completeExceptionally(t);
|
||||||
credentialsProvider,
|
messageFuture.completeExceptionally(t);
|
||||||
userAgent,
|
},
|
||||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
new ProvisioningBlock());
|
||||||
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
|
||||||
userAgent);
|
|
||||||
this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
||||||
var deviceUuid = provisioningApi.getNewDeviceUuid();
|
try {
|
||||||
|
var url = urlFuture.get(30, TimeUnit.SECONDS);
|
||||||
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
return new URI(url);
|
||||||
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
|
throw new TimeoutException("Timed out waiting for provisioning URL");
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Interrupted while waiting for provisioning URL", e);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
throw new IOException("Failed to get provisioning URL", e.getCause());
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new IOException("Invalid provisioning URL", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
||||||
var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
|
SecondaryProvisioningCipher.ProvisioningDecryptResult<ProvisionMessage> decryptResult;
|
||||||
var number = ret.getNumber();
|
try {
|
||||||
var aci = ret.getAci();
|
decryptResult = messageFuture.get(120, TimeUnit.SECONDS);
|
||||||
var pni = ret.getPni();
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
|
throw new TimeoutException("Timed out waiting for provisioning message");
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Interrupted while waiting for provisioning message", e);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
throw new IOException("Failed to receive provisioning message", e.getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(decryptResult instanceof SecondaryProvisioningCipher.ProvisioningDecryptResult.Success<ProvisionMessage> success)) {
|
||||||
|
throw new IOException("Failed to decrypt provisioning message");
|
||||||
|
}
|
||||||
|
var msg = success.getMessage();
|
||||||
|
|
||||||
|
var number = msg.number;
|
||||||
|
var aci = ACI.parseOrThrow(msg.aci, msg.aciBinary);
|
||||||
|
var pni = PNI.parseOrThrow(msg.pni, msg.pniBinary);
|
||||||
|
|
||||||
logger.info("Received link information from {}, linking in progress ...", number);
|
logger.info("Received link information from {}, linking in progress ...", number);
|
||||||
|
|
||||||
@ -120,21 +167,42 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||||||
accountsStore.updateAccount(accountPath, number, aci);
|
accountsStore.updateAccount(accountPath, number, aci);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final IdentityKeyPair aciIdentity;
|
||||||
|
final IdentityKeyPair pniIdentity;
|
||||||
|
final ProfileKey profileKey;
|
||||||
|
try {
|
||||||
|
aciIdentity = new IdentityKeyPair(new IdentityKey(msg.aciIdentityKeyPublic.toByteArray()),
|
||||||
|
new ECPrivateKey(msg.aciIdentityKeyPrivate.toByteArray()));
|
||||||
|
pniIdentity = new IdentityKeyPair(new IdentityKey(msg.pniIdentityKeyPublic.toByteArray()),
|
||||||
|
new ECPrivateKey(msg.pniIdentityKeyPrivate.toByteArray()));
|
||||||
|
profileKey = msg.profileKey == null
|
||||||
|
? KeyUtils.createProfileKey()
|
||||||
|
: new ProfileKey(msg.profileKey.toByteArray());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException("Invalid key material in provisioning message", e);
|
||||||
|
}
|
||||||
|
|
||||||
var encryptedDeviceName = deviceName == null
|
var encryptedDeviceName = deviceName == null
|
||||||
? null
|
? null
|
||||||
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getAciIdentity().getPrivateKey());
|
: DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), aciIdentity);
|
||||||
// Create new account with the synced identity
|
var accountEntropyPool = msg.accountEntropyPool == null ? null : new AccountEntropyPool(msg.accountEntropyPool);
|
||||||
var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
|
var mediaRootBackupKey = msg.mediaRootBackupKey == null
|
||||||
|
? null
|
||||||
|
: new MediaRootBackupKey(msg.mediaRootBackupKey.toByteArray());
|
||||||
|
|
||||||
SignalAccount account = null;
|
SignalAccount account = null;
|
||||||
|
var cleanUpPartialAccountOnFailure = false;
|
||||||
|
var linkingFinished = false;
|
||||||
try {
|
try {
|
||||||
if (!accountExists) {
|
if (!accountExists) {
|
||||||
account = SignalAccount.createLinkedAccount(pathConfig.dataPath(),
|
account = SignalAccount.createLinkedAccount(pathConfig.dataPath(),
|
||||||
accountPath,
|
accountPath,
|
||||||
serviceEnvironmentConfig.type(),
|
serviceEnvironmentConfig.type(),
|
||||||
Settings.DEFAULT);
|
Settings.DEFAULT);
|
||||||
|
cleanUpPartialAccountOnFailure = true;
|
||||||
} else {
|
} else {
|
||||||
account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, Settings.DEFAULT);
|
account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, Settings.DEFAULT);
|
||||||
|
cleanUpPartialAccountOnFailure = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
account.setProvisioningData(number,
|
account.setProvisioningData(number,
|
||||||
@ -142,25 +210,43 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||||||
pni,
|
pni,
|
||||||
password,
|
password,
|
||||||
encryptedDeviceName,
|
encryptedDeviceName,
|
||||||
ret.getAciIdentity(),
|
aciIdentity,
|
||||||
ret.getPniIdentity(),
|
pniIdentity,
|
||||||
profileKey,
|
profileKey,
|
||||||
ret.getMasterKey(),
|
accountEntropyPool,
|
||||||
ret.getAccountEntropyPool(),
|
msg.authCredentialSalt == null ? null : msg.authCredentialSalt.toByteArray(),
|
||||||
ret.getMediaRootBackupKey());
|
mediaRootBackupKey);
|
||||||
|
|
||||||
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
|
if (msg.readReceipts != null) {
|
||||||
|
account.getConfigurationStore().setReadReceipts(msg.readReceipts);
|
||||||
|
}
|
||||||
|
|
||||||
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||||
|
|
||||||
logger.debug("Finishing new device registration");
|
logger.debug("Finishing new device registration");
|
||||||
var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
final var attrs = account.getAccountAttributes(null);
|
||||||
account.getAccountAttributes(null),
|
final var deviceAttributes = new DeviceAttributes(attrs.getFetchesMessages(),
|
||||||
aciPreKeys,
|
attrs.getRegistrationId(),
|
||||||
pniPreKeys);
|
attrs.getPniRegistrationId(),
|
||||||
|
attrs.getName(),
|
||||||
|
attrs.getCapabilities());
|
||||||
|
final var unauthAccountManager = SignalServiceAccountManager.createWithStaticCredentials(
|
||||||
|
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
number,
|
||||||
|
SignalServiceAddress.DEFAULT_DEVICE_ID,
|
||||||
|
password,
|
||||||
|
userAgent,
|
||||||
|
ServiceConfig.AUTOMATIC_NETWORK_RETRY,
|
||||||
|
ServiceConfig.GROUP_MAX_SIZE);
|
||||||
|
final var registerResponse = handleResponseException(unauthAccountManager.getRegistrationApi()
|
||||||
|
.registerAsSecondaryDevice(msg.provisioningCode, deviceAttributes, aciPreKeys, pniPreKeys, null));
|
||||||
|
final var deviceId = Integer.parseInt(registerResponse.getDeviceId());
|
||||||
|
|
||||||
account.finishLinking(deviceId, aciPreKeys, pniPreKeys);
|
account.finishLinking(deviceId, aciPreKeys, pniPreKeys);
|
||||||
|
linkingFinished = true;
|
||||||
|
|
||||||
ManagerImpl m = null;
|
ManagerImpl m = null;
|
||||||
try {
|
try {
|
||||||
@ -197,6 +283,12 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||||||
m.close();
|
m.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (!linkingFinished && cleanUpPartialAccountOnFailure && account != null) {
|
||||||
|
cleanupPartialAccount(account, accountPath, e);
|
||||||
|
account = null;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
account.close();
|
account.close();
|
||||||
@ -204,6 +296,33 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
socketHandle.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupPartialAccount(final SignalAccount account, final String accountPath, final Exception cause) {
|
||||||
|
logger.warn("Link attempt failed before registration completed, removing partial account state for {}.",
|
||||||
|
accountPath,
|
||||||
|
cause);
|
||||||
|
try {
|
||||||
|
account.deleteAccountData();
|
||||||
|
} catch (IOException cleanupError) {
|
||||||
|
logger.warn("Failed to delete partial account data for {}: {}",
|
||||||
|
accountPath,
|
||||||
|
cleanupError.getMessage(),
|
||||||
|
cleanupError);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
accountsStore.removeAccount(accountPath);
|
||||||
|
} catch (RuntimeException cleanupError) {
|
||||||
|
logger.warn("Failed to remove partial account entry for {}: {}",
|
||||||
|
accountPath,
|
||||||
|
cleanupError.getMessage(),
|
||||||
|
cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
|
private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
|
||||||
final SignalAccount signalAccount;
|
final SignalAccount signalAccount;
|
||||||
try {
|
try {
|
||||||
@ -217,6 +336,10 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try (signalAccount) {
|
try (signalAccount) {
|
||||||
|
if (signalAccount.getDeviceId() <= 0) {
|
||||||
|
logger.debug("Account has invalid deviceId {}, allowing relink.", signalAccount.getDeviceId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (signalAccount.isPrimaryDevice()) {
|
if (signalAccount.isPrimaryDevice()) {
|
||||||
logger.debug("Account is a primary device.");
|
logger.debug("Account is a primary device.");
|
||||||
return false;
|
return false;
|
||||||
@ -244,4 +367,36 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ProvisioningBlock implements Function3<CoroutineScope, ProvisioningSocket<ProvisionMessage>, Continuation<? super Unit>, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object invoke(
|
||||||
|
CoroutineScope scope,
|
||||||
|
ProvisioningSocket<ProvisionMessage> socket,
|
||||||
|
Continuation<? super Unit> cont
|
||||||
|
) {
|
||||||
|
Thread.ofVirtual().start(() -> {
|
||||||
|
try {
|
||||||
|
urlFuture.complete(BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||||
|
(s, c) -> socket.getProvisioningUrl(c)));
|
||||||
|
messageFuture.complete(BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||||
|
(s, c) -> socket.getProvisioningMessageDecryptResult(c)));
|
||||||
|
cont.resumeWith(Unit.INSTANCE);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
failBoth(new RuntimeException(e), cont);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
failBoth(t, cont);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return IntrinsicsKt.getCOROUTINE_SUSPENDED();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void failBoth(Throwable t, Continuation<? super Unit> cont) {
|
||||||
|
urlFuture.completeExceptionally(t);
|
||||||
|
messageFuture.completeExceptionally(t);
|
||||||
|
cont.resumeWith(ResultKt.createFailure(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,18 +33,19 @@ import org.asamk.signal.manager.helper.PinHelper;
|
|||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||||
|
import org.signal.core.models.MasterKey;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.MustRequestNewCodeException;
|
||||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||||
|
|
||||||
@ -161,7 +162,11 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||||||
|
|
||||||
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||||
final var result = NumberVerificationUtils.verifyNumber(account.getSessionId(account.getNumber()),
|
final var sessionId = account.getSessionId(account.getNumber());
|
||||||
|
if (sessionId == null) {
|
||||||
|
throw new IOException("No registration verification session active");
|
||||||
|
}
|
||||||
|
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
pin,
|
pin,
|
||||||
pinHelper,
|
pinHelper,
|
||||||
@ -226,6 +231,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||||||
userAgent,
|
userAgent,
|
||||||
account.getCredentialsProvider(),
|
account.getCredentialsProvider(),
|
||||||
account.getSignalServiceDataStore(),
|
account.getSignalServiceDataStore(),
|
||||||
|
0,
|
||||||
null,
|
null,
|
||||||
new ReentrantSignalSessionLock());
|
new ReentrantSignalSessionLock());
|
||||||
handleResponseException(dependencies.getAccountApi()
|
handleResponseException(dependencies.getAccountApi()
|
||||||
@ -258,6 +264,8 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||||||
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
||||||
try {
|
try {
|
||||||
handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
|
handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
|
||||||
|
} catch (MustRequestNewCodeException e) {
|
||||||
|
throw new IOException("Verification code expired, please request a new one by registering again.", e);
|
||||||
} catch (AlreadyVerifiedException e) {
|
} catch (AlreadyVerifiedException e) {
|
||||||
// Already verified so can continue registering
|
// Already verified so can continue registering
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,21 @@ package org.asamk.signal.manager.internal;
|
|||||||
import org.asamk.signal.manager.config.ServiceConfig;
|
import org.asamk.signal.manager.config.ServiceConfig;
|
||||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
|
import org.signal.core.util.UptimeSleepTimer;
|
||||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.protocol.UsePqRatchet;
|
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||||
|
import org.signal.network.api.AttachmentApi;
|
||||||
|
import org.signal.network.api.CallingApi;
|
||||||
|
import org.signal.network.api.CdsApi;
|
||||||
|
import org.signal.network.api.CertificateApi;
|
||||||
|
import org.signal.network.api.LinkDeviceApi;
|
||||||
|
import org.signal.network.api.RateLimitChallengeApi;
|
||||||
|
import org.signal.network.api.UsernameApi;
|
||||||
|
import org.signal.network.rest.SignalRestClient;
|
||||||
|
import org.signal.network.service.CdnService;
|
||||||
|
import org.signal.network.service.StorageServiceService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
@ -15,28 +26,21 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
|||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||||
import org.whispersystems.signalservice.api.account.AccountApi;
|
import org.whispersystems.signalservice.api.account.AccountApi;
|
||||||
import org.whispersystems.signalservice.api.attachment.AttachmentApi;
|
|
||||||
import org.whispersystems.signalservice.api.cds.CdsApi;
|
|
||||||
import org.whispersystems.signalservice.api.certificate.CertificateApi;
|
|
||||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||||
import org.whispersystems.signalservice.api.keys.KeysApi;
|
import org.whispersystems.signalservice.api.keys.KeysApi;
|
||||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
import org.whispersystems.signalservice.api.keys.PreKeyRepository;
|
||||||
import org.whispersystems.signalservice.api.message.MessageApi;
|
import org.whispersystems.signalservice.api.message.MessageApi;
|
||||||
import org.whispersystems.signalservice.api.profiles.ProfileApi;
|
import org.whispersystems.signalservice.api.profiles.ProfileApi;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi;
|
|
||||||
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
||||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
|
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
|
|
||||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||||
import org.whispersystems.signalservice.api.username.UsernameApi;
|
|
||||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
|
||||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
||||||
@ -45,6 +49,7 @@ import java.io.IOException;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Proxy;
|
import java.net.Proxy;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -60,6 +65,7 @@ public class SignalDependencies {
|
|||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
private final CredentialsProvider credentialsProvider;
|
private final CredentialsProvider credentialsProvider;
|
||||||
private final SignalServiceDataStore dataStore;
|
private final SignalServiceDataStore dataStore;
|
||||||
|
private final int deviceId;
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final SignalSessionLock sessionLock;
|
private final SignalSessionLock sessionLock;
|
||||||
|
|
||||||
@ -76,10 +82,16 @@ public class SignalDependencies {
|
|||||||
private StorageServiceApi storageServiceApi;
|
private StorageServiceApi storageServiceApi;
|
||||||
private CertificateApi certificateApi;
|
private CertificateApi certificateApi;
|
||||||
private AttachmentApi attachmentApi;
|
private AttachmentApi attachmentApi;
|
||||||
|
private CallingApi callingApi;
|
||||||
private MessageApi messageApi;
|
private MessageApi messageApi;
|
||||||
private KeysApi keysApi;
|
private KeysApi keysApi;
|
||||||
private GroupsV2Operations groupsV2Operations;
|
private GroupsV2Operations groupsV2Operations;
|
||||||
private ClientZkOperations clientZkOperations;
|
private ClientZkOperations clientZkOperations;
|
||||||
|
private ProfileService profileService;
|
||||||
|
private ProfileApi profileApi;
|
||||||
|
private CdnService cdnService;
|
||||||
|
private PreKeyRepository preKeyRepository;
|
||||||
|
private SignalRestClient signalRestClient;
|
||||||
|
|
||||||
private PushServiceSocket pushServiceSocket;
|
private PushServiceSocket pushServiceSocket;
|
||||||
private Network libSignalNetwork;
|
private Network libSignalNetwork;
|
||||||
@ -89,14 +101,13 @@ public class SignalDependencies {
|
|||||||
private SignalServiceMessageSender messageSender;
|
private SignalServiceMessageSender messageSender;
|
||||||
|
|
||||||
private List<SecureValueRecovery> secureValueRecovery;
|
private List<SecureValueRecovery> secureValueRecovery;
|
||||||
private ProfileService profileService;
|
|
||||||
private ProfileApi profileApi;
|
|
||||||
|
|
||||||
SignalDependencies(
|
SignalDependencies(
|
||||||
final ServiceEnvironmentConfig serviceEnvironmentConfig,
|
final ServiceEnvironmentConfig serviceEnvironmentConfig,
|
||||||
final String userAgent,
|
final String userAgent,
|
||||||
final CredentialsProvider credentialsProvider,
|
final CredentialsProvider credentialsProvider,
|
||||||
final SignalServiceDataStore dataStore,
|
final SignalServiceDataStore dataStore,
|
||||||
|
final int deviceId,
|
||||||
final ExecutorService executor,
|
final ExecutorService executor,
|
||||||
final SignalSessionLock sessionLock
|
final SignalSessionLock sessionLock
|
||||||
) {
|
) {
|
||||||
@ -104,6 +115,7 @@ public class SignalDependencies {
|
|||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.credentialsProvider = credentialsProvider;
|
this.credentialsProvider = credentialsProvider;
|
||||||
this.dataStore = dataStore;
|
this.dataStore = dataStore;
|
||||||
|
this.deviceId = deviceId;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.sessionLock = sessionLock;
|
this.sessionLock = sessionLock;
|
||||||
}
|
}
|
||||||
@ -153,7 +165,10 @@ public class SignalDependencies {
|
|||||||
|
|
||||||
public Network getLibSignalNetwork() {
|
public Network getLibSignalNetwork() {
|
||||||
return getOrCreate(() -> libSignalNetwork, () -> {
|
return getOrCreate(() -> libSignalNetwork, () -> {
|
||||||
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
|
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(),
|
||||||
|
userAgent,
|
||||||
|
Map.of(),
|
||||||
|
Network.BuildVariant.PRODUCTION);
|
||||||
setSignalNetworkProxy(libSignalNetwork);
|
setSignalNetworkProxy(libSignalNetwork);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -238,8 +253,8 @@ public class SignalDependencies {
|
|||||||
getPushServiceSocket()));
|
getPushServiceSocket()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public StorageServiceRepository getStorageServiceRepository() {
|
public StorageServiceService getStorageServiceRepository() {
|
||||||
return new StorageServiceRepository(getStorageServiceApi());
|
return new StorageServiceService(getStorageServiceApi());
|
||||||
}
|
}
|
||||||
|
|
||||||
public CertificateApi getCertificateApi() {
|
public CertificateApi getCertificateApi() {
|
||||||
@ -252,6 +267,13 @@ public class SignalDependencies {
|
|||||||
() -> attachmentApi = new AttachmentApi(getAuthenticatedSignalWebSocket(), getPushServiceSocket()));
|
() -> attachmentApi = new AttachmentApi(getAuthenticatedSignalWebSocket(), getPushServiceSocket()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallingApi getCallingApi() {
|
||||||
|
return getOrCreate(() -> callingApi,
|
||||||
|
() -> callingApi = new CallingApi(getAuthenticatedSignalWebSocket(),
|
||||||
|
getUnauthenticatedSignalWebSocket(),
|
||||||
|
getPushServiceSocket()));
|
||||||
|
}
|
||||||
|
|
||||||
public MessageApi getMessageApi() {
|
public MessageApi getMessageApi() {
|
||||||
return getOrCreate(() -> messageApi,
|
return getOrCreate(() -> messageApi,
|
||||||
() -> messageApi = new MessageApi(getAuthenticatedSignalWebSocket(),
|
() -> messageApi = new MessageApi(getAuthenticatedSignalWebSocket(),
|
||||||
@ -289,7 +311,7 @@ public class SignalDependencies {
|
|||||||
getLibSignalNetwork(),
|
getLibSignalNetwork(),
|
||||||
credentialsProvider,
|
credentialsProvider,
|
||||||
allowStories,
|
allowStories,
|
||||||
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(30));
|
||||||
healthMonitor.monitor(authenticatedSignalWebSocket);
|
healthMonitor.monitor(authenticatedSignalWebSocket);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -304,7 +326,7 @@ public class SignalDependencies {
|
|||||||
getLibSignalNetwork(),
|
getLibSignalNetwork(),
|
||||||
null,
|
null,
|
||||||
allowStories,
|
allowStories,
|
||||||
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(30));
|
||||||
healthMonitor.monitor(unauthenticatedSignalWebSocket);
|
healthMonitor.monitor(unauthenticatedSignalWebSocket);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -314,19 +336,42 @@ public class SignalDependencies {
|
|||||||
() -> messageReceiver = new SignalServiceMessageReceiver(getPushServiceSocket()));
|
() -> messageReceiver = new SignalServiceMessageReceiver(getPushServiceSocket()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SignalRestClient getSignalRestClient() {
|
||||||
|
return getOrCreate(() -> signalRestClient,
|
||||||
|
() -> signalRestClient = new SignalRestClient(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
|
userAgent,
|
||||||
|
credentialsProvider,
|
||||||
|
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CdnService getCdnService() {
|
||||||
|
return getOrCreate(() -> cdnService,
|
||||||
|
() -> cdnService = new CdnService(getSignalRestClient(), getAttachmentApi()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreKeyRepository getPreKeyRepository() {
|
||||||
|
final SignalProtocolAddress localProtocolAddress = credentialsProvider.getAci().toProtocolAddress(deviceId);
|
||||||
|
return getOrCreate(() -> preKeyRepository,
|
||||||
|
() -> preKeyRepository = new PreKeyRepository(getKeysApi(),
|
||||||
|
dataStore.aci(),
|
||||||
|
localProtocolAddress,
|
||||||
|
getSessionLock(),
|
||||||
|
Runnable::run));
|
||||||
|
}
|
||||||
|
|
||||||
public SignalServiceMessageSender getMessageSender() {
|
public SignalServiceMessageSender getMessageSender() {
|
||||||
return getOrCreate(() -> messageSender,
|
return getOrCreate(() -> messageSender,
|
||||||
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
|
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
|
||||||
dataStore,
|
dataStore,
|
||||||
sessionLock,
|
sessionLock,
|
||||||
getAttachmentApi(),
|
|
||||||
getMessageApi(),
|
getMessageApi(),
|
||||||
getKeysApi(),
|
getKeysApi(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
executor,
|
executor,
|
||||||
ServiceConfig.MAX_ENVELOPE_SIZE,
|
ServiceConfig.MAX_ENVELOPE_SIZE,
|
||||||
|
ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE,
|
||||||
() -> true,
|
() -> true,
|
||||||
UsePqRatchet.NO));
|
getPreKeyRepository()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SecureValueRecovery> getSecureValueRecovery() {
|
public List<SecureValueRecovery> getSecureValueRecovery() {
|
||||||
@ -354,7 +399,10 @@ public class SignalDependencies {
|
|||||||
|
|
||||||
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
|
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
|
||||||
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots());
|
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots());
|
||||||
final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164());
|
final var serviceId = serviceIdType == ServiceIdType.ACI
|
||||||
|
? credentialsProvider.getAci()
|
||||||
|
: credentialsProvider.getPni();
|
||||||
|
final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164());
|
||||||
final var deviceId = credentialsProvider.getDeviceId();
|
final var deviceId = credentialsProvider.getDeviceId();
|
||||||
return new SignalServiceCipher(address,
|
return new SignalServiceCipher(address,
|
||||||
deviceId,
|
deviceId,
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
package org.asamk.signal.manager.internal;
|
||||||
|
|
||||||
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class SignalLogger extends Log.Logger {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger("LibSignalService");
|
||||||
|
|
||||||
|
public static void initLogger() {
|
||||||
|
Log.initialize(() -> true, new SignalLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalLogger() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void v(final String tag, final String message, final Throwable throwable, final boolean b) {
|
||||||
|
logger.trace("[{}]: {}", tag, message, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void d(final String tag, final String message, final Throwable throwable, final boolean b) {
|
||||||
|
logger.debug("[{}]: {}", tag, message, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void i(final String tag, final String message, final Throwable throwable, final boolean b) {
|
||||||
|
logger.info("[{}]: {}", tag, message, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void w(final String tag, final String message, final Throwable throwable, final boolean b) {
|
||||||
|
logger.warn("[{}]: {}", tag, message, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void e(final String tag, final String message, final Throwable throwable, final boolean b) {
|
||||||
|
logger.error("[{}]: {}", tag, message, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,15 @@
|
|||||||
package org.asamk.signal.manager.internal;
|
package org.asamk.signal.manager.internal;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.signal.core.util.SleepTimer;
|
||||||
|
import org.signal.network.util.Preconditions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
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.HealthMonitor;
|
||||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -94,6 +96,26 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
|||||||
return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives();
|
return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedAlerts(@NotNull final String[] strings, final boolean b) {
|
||||||
|
if (strings.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info("Received alerts: {}", String.join(", ", strings));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServerTimestamp(final long serverTimestamp, final boolean isIdentifiedWebsocket) {
|
||||||
|
final var skew = skewFrom(serverTimestamp);
|
||||||
|
if (skew.compareTo(Duration.ofDays(1)) > 0) {
|
||||||
|
logger.warn("Local clock is off from the server by {}, which exceeds the allowed limit..", skew);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Duration skewFrom(long serverTime) {
|
||||||
|
return Duration.ofMillis(Math.abs(System.currentTimeMillis() - serverTime));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
|
* Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
|
||||||
* the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
|
* the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
|
||||||
|
|||||||
@ -2,10 +2,10 @@ package org.asamk.signal.manager.jobs;
|
|||||||
|
|
||||||
import org.asamk.signal.manager.api.StickerPackId;
|
import org.asamk.signal.manager.api.StickerPackId;
|
||||||
import org.asamk.signal.manager.helper.Context;
|
import org.asamk.signal.manager.helper.Context;
|
||||||
|
import org.signal.core.util.Hex;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|||||||
@ -16,11 +16,11 @@ import org.asamk.signal.manager.storage.senderKeys.SenderKeyRecordStore;
|
|||||||
import org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore;
|
import org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore;
|
||||||
import org.asamk.signal.manager.storage.sessions.SessionStore;
|
import org.asamk.signal.manager.storage.sessions.SessionStore;
|
||||||
import org.asamk.signal.manager.storage.stickers.StickerStore;
|
import org.asamk.signal.manager.storage.stickers.StickerStore;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@ -33,7 +33,7 @@ import java.util.UUID;
|
|||||||
public class AccountDatabase extends Database {
|
public class AccountDatabase extends Database {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||||
private static final long DATABASE_VERSION = 28;
|
private static final long DATABASE_VERSION = 31;
|
||||||
|
|
||||||
private AccountDatabase(final HikariDataSource dataSource) {
|
private AccountDatabase(final HikariDataSource dataSource) {
|
||||||
super(logger, DATABASE_VERSION, dataSource);
|
super(logger, DATABASE_VERSION, dataSource);
|
||||||
@ -623,6 +623,36 @@ public class AccountDatabase extends Database {
|
|||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 29) {
|
||||||
|
logger.debug("Updating database: Adding sticker storage sync columns");
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("""
|
||||||
|
ALTER TABLE sticker ADD COLUMN position INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE sticker ADD COLUMN deleted_timestamp INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE sticker ADD COLUMN storage_id BLOB;
|
||||||
|
ALTER TABLE sticker ADD COLUMN storage_record BLOB;
|
||||||
|
CREATE UNIQUE INDEX sticker_storage_id_index ON sticker (storage_id);
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 30) {
|
||||||
|
logger.debug("Updating database: Create pni_signature_verified column");
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("""
|
||||||
|
ALTER TABLE recipient ADD pni_signature_verified INTEGER NOT NULL DEFAULT FALSE;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 31) {
|
||||||
|
logger.debug("Updating database: Add blocked-at timestamps");
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("""
|
||||||
|
ALTER TABLE recipient ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE group_v1 ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE group_v2 ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createUuidMappingTable(
|
private static void createUuidMappingTable(
|
||||||
@ -646,9 +676,10 @@ public class AccountDatabase extends Database {
|
|||||||
try (final var preparedStatement = connection.prepareStatement(sql)) {
|
try (final var preparedStatement = connection.prepareStatement(sql)) {
|
||||||
try (var result = Utils.executeQueryForStream(preparedStatement, (resultSet) -> {
|
try (var result = Utils.executeQueryForStream(preparedStatement, (resultSet) -> {
|
||||||
final var pni = Optional.ofNullable(resultSet.getBytes("pni"))
|
final var pni = Optional.ofNullable(resultSet.getBytes("pni"))
|
||||||
.map(UuidUtil::parseOrNull)
|
.map(UuidUtil.INSTANCE::parseOrNull)
|
||||||
.map(ServiceId.PNI::from);
|
.map(ServiceId.PNI::from);
|
||||||
final var serviceIdUuid = Optional.ofNullable(resultSet.getBytes("uuid")).map(UuidUtil::parseOrNull);
|
final var serviceIdUuid = Optional.ofNullable(resultSet.getBytes("uuid"))
|
||||||
|
.map(UuidUtil.INSTANCE::parseOrNull);
|
||||||
final var serviceId = serviceIdUuid.isPresent() && pni.isPresent() && serviceIdUuid.get()
|
final var serviceId = serviceIdUuid.isPresent() && pni.isPresent() && serviceIdUuid.get()
|
||||||
.equals(pni.get().getRawUuid())
|
.equals(pni.get().getRawUuid())
|
||||||
? pni.<ServiceId>map(p -> p)
|
? pni.<ServiceId>map(p -> p)
|
||||||
|
|||||||
@ -44,7 +44,8 @@ public class AttachmentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public StreamDetails retrieveAttachment(final String id) throws IOException {
|
public StreamDetails retrieveAttachment(final String id) throws IOException {
|
||||||
final var attachmentFile = new File(attachmentsPath, id);
|
final var safeId = sanitizeId(id);
|
||||||
|
final var attachmentFile = new File(attachmentsPath, safeId);
|
||||||
return Utils.createStreamDetailsFromFile(attachmentFile);
|
return Utils.createStreamDetailsFromFile(attachmentFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +62,8 @@ public class AttachmentStore {
|
|||||||
Optional<String> contentType
|
Optional<String> contentType
|
||||||
) {
|
) {
|
||||||
final var extension = getAttachmentExtension(filename, contentType);
|
final var extension = getAttachmentExtension(filename, contentType);
|
||||||
return new File(attachmentsPath, attachmentId.toString() + extension + ".preview");
|
final var safe = sanitizeId(attachmentId.toString());
|
||||||
|
return new File(attachmentsPath, safe + extension + ".preview");
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getAttachmentFile(
|
private File getAttachmentFile(
|
||||||
@ -70,7 +72,15 @@ public class AttachmentStore {
|
|||||||
Optional<String> contentType
|
Optional<String> contentType
|
||||||
) {
|
) {
|
||||||
final var extension = getAttachmentExtension(filename, contentType);
|
final var extension = getAttachmentExtension(filename, contentType);
|
||||||
return new File(attachmentsPath, attachmentId.toString() + extension);
|
final var safe = sanitizeId(attachmentId.toString());
|
||||||
|
return new File(attachmentsPath, safe + 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) {
|
||||||
|
|||||||
@ -53,6 +53,14 @@ import org.asamk.signal.manager.storage.stickers.StickerStore;
|
|||||||
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
|
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
|
import org.signal.core.models.AccountEntropyPool;
|
||||||
|
import org.signal.core.models.MasterKey;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
|
import org.signal.core.models.backup.MediaRootBackupKey;
|
||||||
|
import org.signal.core.models.storageservice.StorageKey;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||||
@ -65,24 +73,16 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
|
|||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.AccountEntropyPool;
|
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
|
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
|
||||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@ -116,7 +116,7 @@ public class SignalAccount implements Closeable {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||||
|
|
||||||
private static final int MINIMUM_STORAGE_VERSION = 1;
|
private static final int MINIMUM_STORAGE_VERSION = 1;
|
||||||
private static final int CURRENT_STORAGE_VERSION = 10;
|
private static final int CURRENT_STORAGE_VERSION = 11;
|
||||||
|
|
||||||
private final Object LOCK = new Object();
|
private final Object LOCK = new Object();
|
||||||
|
|
||||||
@ -141,6 +141,7 @@ public class SignalAccount implements Closeable {
|
|||||||
private MasterKey pinMasterKey;
|
private MasterKey pinMasterKey;
|
||||||
private StorageKey storageKey;
|
private StorageKey storageKey;
|
||||||
private AccountEntropyPool accountEntropyPool;
|
private AccountEntropyPool accountEntropyPool;
|
||||||
|
private byte[] authCredentialSalt;
|
||||||
private MediaRootBackupKey mediaRootBackupKey;
|
private MediaRootBackupKey mediaRootBackupKey;
|
||||||
private ProfileKey profileKey;
|
private ProfileKey profileKey;
|
||||||
|
|
||||||
@ -153,6 +154,10 @@ public class SignalAccount implements Closeable {
|
|||||||
private final KeyValueEntry<Long> lastReceiveTimestamp = new KeyValueEntry<>("last-receive-timestamp",
|
private final KeyValueEntry<Long> lastReceiveTimestamp = new KeyValueEntry<>("last-receive-timestamp",
|
||||||
long.class,
|
long.class,
|
||||||
0L);
|
0L);
|
||||||
|
private final KeyValueEntry<Long> lastAppliedPniChangeServerTimestamp = new KeyValueEntry<>(
|
||||||
|
"last-applied-pni-change-server-timestamp",
|
||||||
|
long.class,
|
||||||
|
0L);
|
||||||
private final KeyValueEntry<Boolean> needsToRetryFailedMessages = new KeyValueEntry<>("retry-failed-messages",
|
private final KeyValueEntry<Boolean> needsToRetryFailedMessages = new KeyValueEntry<>("retry-failed-messages",
|
||||||
Boolean.class,
|
Boolean.class,
|
||||||
true);
|
true);
|
||||||
@ -192,6 +197,10 @@ public class SignalAccount implements Closeable {
|
|||||||
this.lock = lock;
|
this.lock = lock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public File getDataPath() {
|
||||||
|
return dataPath;
|
||||||
|
}
|
||||||
|
|
||||||
public static SignalAccount load(
|
public static SignalAccount load(
|
||||||
File dataPath,
|
File dataPath,
|
||||||
String accountPath,
|
String accountPath,
|
||||||
@ -288,12 +297,12 @@ public class SignalAccount implements Closeable {
|
|||||||
final ACI aci,
|
final ACI aci,
|
||||||
final PNI pni,
|
final PNI pni,
|
||||||
final String password,
|
final String password,
|
||||||
final String encryptedDeviceName,
|
final byte[] encryptedDeviceName,
|
||||||
final IdentityKeyPair aciIdentity,
|
final IdentityKeyPair aciIdentity,
|
||||||
final IdentityKeyPair pniIdentity,
|
final IdentityKeyPair pniIdentity,
|
||||||
final ProfileKey profileKey,
|
final ProfileKey profileKey,
|
||||||
final MasterKey masterKey,
|
|
||||||
final AccountEntropyPool accountEntropyPool,
|
final AccountEntropyPool accountEntropyPool,
|
||||||
|
final byte[] authCredentialSalt,
|
||||||
final MediaRootBackupKey mediaRootBackupKey
|
final MediaRootBackupKey mediaRootBackupKey
|
||||||
) {
|
) {
|
||||||
this.deviceId = 0;
|
this.deviceId = 0;
|
||||||
@ -304,19 +313,21 @@ public class SignalAccount implements Closeable {
|
|||||||
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.profileKey = profileKey;
|
this.profileKey = profileKey;
|
||||||
this.encryptedDeviceName = encryptedDeviceName;
|
this.encryptedDeviceName = org.signal.core.util.Base64.encodeWithoutPadding(encryptedDeviceName);
|
||||||
this.aciAccountData.setIdentityKeyPair(aciIdentity);
|
this.aciAccountData.setIdentityKeyPair(aciIdentity);
|
||||||
this.pniAccountData.setIdentityKeyPair(pniIdentity);
|
this.pniAccountData.setIdentityKeyPair(pniIdentity);
|
||||||
this.registered = false;
|
this.registered = false;
|
||||||
this.isMultiDevice = true;
|
this.isMultiDevice = true;
|
||||||
setLastReceiveTimestamp(0L);
|
setLastReceiveTimestamp(0L);
|
||||||
|
setLastAppliedPniChangeServerTimestamp(0L);
|
||||||
if (accountEntropyPool != null) {
|
if (accountEntropyPool != null) {
|
||||||
this.pinMasterKey = null;
|
this.pinMasterKey = null;
|
||||||
this.accountEntropyPool = accountEntropyPool;
|
this.accountEntropyPool = accountEntropyPool;
|
||||||
} else {
|
} else {
|
||||||
this.pinMasterKey = masterKey;
|
this.pinMasterKey = null;
|
||||||
this.accountEntropyPool = null;
|
this.accountEntropyPool = null;
|
||||||
}
|
}
|
||||||
|
this.authCredentialSalt = authCredentialSalt;
|
||||||
this.mediaRootBackupKey = mediaRootBackupKey;
|
this.mediaRootBackupKey = mediaRootBackupKey;
|
||||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||||
this.setStorageManifest(null);
|
this.setStorageManifest(null);
|
||||||
@ -353,6 +364,7 @@ public class SignalAccount implements Closeable {
|
|||||||
) {
|
) {
|
||||||
this.pinMasterKey = masterKey;
|
this.pinMasterKey = masterKey;
|
||||||
this.accountEntropyPool = null;
|
this.accountEntropyPool = null;
|
||||||
|
this.authCredentialSalt = null;
|
||||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||||
this.setStorageManifest(null);
|
this.setStorageManifest(null);
|
||||||
this.storageKey = null;
|
this.storageKey = null;
|
||||||
@ -365,6 +377,7 @@ public class SignalAccount implements Closeable {
|
|||||||
init();
|
init();
|
||||||
this.registrationLockPin = pin;
|
this.registrationLockPin = pin;
|
||||||
setLastReceiveTimestamp(0L);
|
setLastReceiveTimestamp(0L);
|
||||||
|
setLastAppliedPniChangeServerTimestamp(0L);
|
||||||
save();
|
save();
|
||||||
|
|
||||||
setPreKeys(ServiceIdType.ACI, aciPreKeys);
|
setPreKeys(ServiceIdType.ACI, aciPreKeys);
|
||||||
@ -376,6 +389,7 @@ public class SignalAccount implements Closeable {
|
|||||||
trustSelfIdentity(ServiceIdType.ACI);
|
trustSelfIdentity(ServiceIdType.ACI);
|
||||||
trustSelfIdentity(ServiceIdType.PNI);
|
trustSelfIdentity(ServiceIdType.PNI);
|
||||||
getKeyValueStore().storeEntry(lastRecipientsRefresh, null);
|
getKeyValueStore().storeEntry(lastRecipientsRefresh, null);
|
||||||
|
clearSessionId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initDatabase() {
|
public void initDatabase() {
|
||||||
@ -516,6 +530,9 @@ public class SignalAccount implements Closeable {
|
|||||||
if (storage.accountEntropyPool != null) {
|
if (storage.accountEntropyPool != null) {
|
||||||
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
||||||
}
|
}
|
||||||
|
if (storage.authCredentialSalt != null) {
|
||||||
|
authCredentialSalt = base64.decode(storage.authCredentialSalt);
|
||||||
|
}
|
||||||
if (storage.mediaRootBackupKey != null) {
|
if (storage.mediaRootBackupKey != null) {
|
||||||
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
||||||
}
|
}
|
||||||
@ -896,6 +913,7 @@ public class SignalAccount implements Closeable {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
contact.blocked,
|
contact.blocked,
|
||||||
|
0,
|
||||||
contact.archived,
|
contact.archived,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -941,7 +959,7 @@ public class SignalAccount implements Closeable {
|
|||||||
profile.isUnrestrictedUnidentifiedAccess()
|
profile.isUnrestrictedUnidentifiedAccess()
|
||||||
? Profile.UnidentifiedAccessMode.UNRESTRICTED
|
? Profile.UnidentifiedAccessMode.UNRESTRICTED
|
||||||
: profile.getUnidentifiedAccess() != null
|
: profile.getUnidentifiedAccess() != null
|
||||||
? Profile.UnidentifiedAccessMode.ENABLED
|
? Profile.UnidentifiedAccessMode.ENABLED
|
||||||
: Profile.UnidentifiedAccessMode.DISABLED,
|
: Profile.UnidentifiedAccessMode.DISABLED,
|
||||||
capabilities,
|
capabilities,
|
||||||
null);
|
null);
|
||||||
@ -961,7 +979,7 @@ public class SignalAccount implements Closeable {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
|
if (UuidUtil.INSTANCE.isUuid(thread.id) || thread.id.startsWith("+")) {
|
||||||
final var recipientId = getRecipientResolver().resolveRecipient(thread.id);
|
final var recipientId = getRecipientResolver().resolveRecipient(thread.id);
|
||||||
var contact = getContactStore().getContact(recipientId);
|
var contact = getContactStore().getContact(recipientId);
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
@ -1004,6 +1022,7 @@ public class SignalAccount implements Closeable {
|
|||||||
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
||||||
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
||||||
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
||||||
|
authCredentialSalt == null ? null : base64.encodeToString(authCredentialSalt),
|
||||||
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
||||||
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
||||||
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
||||||
@ -1218,6 +1237,11 @@ public class SignalAccount implements Closeable {
|
|||||||
return pniAccountData.getSignalServiceAccountDataStore();
|
return pniAccountData.getSignalServiceAccountDataStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignalServiceAccountDataStore pniOrNull() {
|
||||||
|
return getPni() != null ? pniAccountData.getSignalServiceAccountDataStore() : null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isMultiDevice() {
|
public boolean isMultiDevice() {
|
||||||
return SignalAccount.this.isMultiDevice();
|
return SignalAccount.this.isMultiDevice();
|
||||||
@ -1485,6 +1509,12 @@ public class SignalAccount implements Closeable {
|
|||||||
keyValueStore.storeEntry(verificationSessionId, sessionId);
|
keyValueStore.storeEntry(verificationSessionId, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clearSessionId() {
|
||||||
|
final var keyValueStore = getKeyValueStore();
|
||||||
|
keyValueStore.storeEntry(verificationSessionNumber, null);
|
||||||
|
keyValueStore.storeEntry(verificationSessionId, null);
|
||||||
|
}
|
||||||
|
|
||||||
public void setEncryptedDeviceName(final String encryptedDeviceName) {
|
public void setEncryptedDeviceName(final String encryptedDeviceName) {
|
||||||
this.encryptedDeviceName = encryptedDeviceName;
|
this.encryptedDeviceName = encryptedDeviceName;
|
||||||
save();
|
save();
|
||||||
@ -1632,6 +1662,10 @@ public class SignalAccount implements Closeable {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getAuthCredentialSalt() {
|
||||||
|
return authCredentialSalt;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRecoveryPassword() {
|
public String getRecoveryPassword() {
|
||||||
final var masterKey = getPinBackedMasterKey();
|
final var masterKey = getPinBackedMasterKey();
|
||||||
if (masterKey == null) {
|
if (masterKey == null) {
|
||||||
@ -1715,7 +1749,7 @@ public class SignalAccount implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRegistered() {
|
public boolean isRegistered() {
|
||||||
return registered;
|
return registered && deviceId > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegistered(final boolean registered) {
|
public void setRegistered(final boolean registered) {
|
||||||
@ -1743,6 +1777,14 @@ public class SignalAccount implements Closeable {
|
|||||||
getKeyValueStore().storeEntry(lastReceiveTimestamp, value);
|
getKeyValueStore().storeEntry(lastReceiveTimestamp, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getLastAppliedPniChangeServerTimestamp() {
|
||||||
|
return getKeyValueStore().getEntry(lastAppliedPniChangeServerTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastAppliedPniChangeServerTimestamp(final long value) {
|
||||||
|
getKeyValueStore().storeEntry(lastAppliedPniChangeServerTimestamp, value);
|
||||||
|
}
|
||||||
|
|
||||||
public void setNeedsToRetryFailedMessages(final boolean value) {
|
public void setNeedsToRetryFailedMessages(final boolean value) {
|
||||||
getKeyValueStore().storeEntry(needsToRetryFailedMessages, value);
|
getKeyValueStore().storeEntry(needsToRetryFailedMessages, value);
|
||||||
}
|
}
|
||||||
@ -1914,7 +1956,8 @@ public class SignalAccount implements Closeable {
|
|||||||
getSessionStore(),
|
getSessionStore(),
|
||||||
getIdentityKeyStore(),
|
getIdentityKeyStore(),
|
||||||
getSenderKeyStore(),
|
getSenderKeyStore(),
|
||||||
SignalAccount.this::isMultiDevice));
|
SignalAccount.this::isMultiDevice,
|
||||||
|
SignalAccount.this::setMultiDevice));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreKeyStore getPreKeyStore() {
|
public PreKeyStore getPreKeyStore() {
|
||||||
@ -1962,6 +2005,7 @@ public class SignalAccount implements Closeable {
|
|||||||
String pinMasterKey,
|
String pinMasterKey,
|
||||||
String storageKey,
|
String storageKey,
|
||||||
String accountEntropyPool,
|
String accountEntropyPool,
|
||||||
|
String authCredentialSalt,
|
||||||
String mediaRootBackupKey,
|
String mediaRootBackupKey,
|
||||||
String profileKey,
|
String profileKey,
|
||||||
String usernameLinkEntropy,
|
String usernameLinkEntropy,
|
||||||
|
|||||||
@ -10,11 +10,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.io.InvalidObjectException;
|
import java.io.InvalidObjectException;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@ -57,7 +57,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static RecipientAddress getRecipientAddressFromLegacyIdentifier(final String identifier) {
|
public static RecipientAddress getRecipientAddressFromLegacyIdentifier(final String identifier) {
|
||||||
if (UuidUtil.isUuid(identifier)) {
|
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
||||||
return new RecipientAddress(ServiceId.parseOrThrow(identifier));
|
return new RecipientAddress(ServiceId.parseOrThrow(identifier));
|
||||||
} else {
|
} else {
|
||||||
return new RecipientAddress(Optional.empty(), Optional.of(identifier));
|
return new RecipientAddress(Optional.empty(), Optional.of(identifier));
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import org.asamk.signal.manager.api.ServiceEnvironment;
|
|||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.Utils;
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import org.asamk.signal.manager.api.GroupPermission;
|
|||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
@ -24,7 +25,18 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
|
|
||||||
public abstract GroupInviteLinkUrl getGroupInviteLink();
|
public abstract GroupInviteLinkUrl getGroupInviteLink();
|
||||||
|
|
||||||
public abstract Set<RecipientId> getMembers();
|
public abstract Collection<GroupMemberInfo> getMembers();
|
||||||
|
|
||||||
|
public Set<RecipientId> getMemberRecipientIds() {
|
||||||
|
return getMembers().stream().map(GroupMemberInfo::getRecipientId).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public GroupMemberInfo getMember(RecipientId recipientId) {
|
||||||
|
return getMembers().stream()
|
||||||
|
.filter(member -> member.getRecipientId().equals(recipientId))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
public Set<RecipientId> getBannedMembers() {
|
public Set<RecipientId> getBannedMembers() {
|
||||||
return Set.of();
|
return Set.of();
|
||||||
@ -38,7 +50,7 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
return Set.of();
|
return Set.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<RecipientId> getAdminMembers() {
|
public Set<RecipientId> getAdminMemberRecipientIds() {
|
||||||
return Set.of();
|
return Set.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +58,10 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
|
|
||||||
public abstract void setBlocked(boolean blocked);
|
public abstract void setBlocked(boolean blocked);
|
||||||
|
|
||||||
|
public abstract long getBlockedAt();
|
||||||
|
|
||||||
|
public abstract void setBlockedAt(long blockedAt);
|
||||||
|
|
||||||
public abstract boolean isProfileSharingEnabled();
|
public abstract boolean isProfileSharingEnabled();
|
||||||
|
|
||||||
public abstract void setProfileSharingEnabled(boolean profileSharingEnabled);
|
public abstract void setProfileSharingEnabled(boolean profileSharingEnabled);
|
||||||
@ -54,6 +70,8 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
|
|
||||||
public abstract boolean isAnnouncementGroup();
|
public abstract boolean isAnnouncementGroup();
|
||||||
|
|
||||||
|
public abstract boolean isTerminated();
|
||||||
|
|
||||||
public abstract GroupPermission getPermissionAddMember();
|
public abstract GroupPermission getPermissionAddMember();
|
||||||
|
|
||||||
public abstract GroupPermission getPermissionEditDetails();
|
public abstract GroupPermission getPermissionEditDetails();
|
||||||
@ -61,21 +79,23 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
public abstract GroupPermission getPermissionSendMessage();
|
public abstract GroupPermission getPermissionSendMessage();
|
||||||
|
|
||||||
public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
|
public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
|
||||||
return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
|
return getMemberRecipientIds().stream()
|
||||||
|
.filter(member -> !member.equals(recipientId))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<RecipientId> getMembersIncludingPendingWithout(RecipientId recipientId) {
|
public Set<RecipientId> getMembersIncludingPendingWithout(RecipientId recipientId) {
|
||||||
return Stream.concat(getMembers().stream(), getPendingMembers().stream())
|
return Stream.concat(getMemberRecipientIds().stream(), getPendingMembers().stream())
|
||||||
.filter(member -> !member.equals(recipientId))
|
.filter(member -> !member.equals(recipientId))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMember(RecipientId recipientId) {
|
public boolean isMember(RecipientId recipientId) {
|
||||||
return getMembers().contains(recipientId);
|
return getMembers().stream().anyMatch(m -> m.getRecipientId().equals(recipientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAdmin(RecipientId recipientId) {
|
public boolean isAdmin(RecipientId recipientId) {
|
||||||
return getAdminMembers().contains(recipientId);
|
return getMembers().stream().anyMatch(m -> m.isAdmin() && m.getRecipientId().equals(recipientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPendingMember(RecipientId recipientId) {
|
public boolean isPendingMember(RecipientId recipientId) {
|
||||||
|
|||||||
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