mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-08-29 06:06:22 +00:00
Compare commits
No commits in common. "master" and "v0.14.2" have entirely different histories.
57
.github/workflows/build.yml
vendored
57
.github/workflows/build.yml
vendored
@ -1,57 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
pull_request:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
# java="25" is the LTS Java version used in reproducible builds script (default in Containerfile).
|
|
||||||
# More Java versions can be added to test compatibility, eg. "26".
|
|
||||||
java: ["25", "26"]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
if [ "${{ matrix.java }}" != "25" ]; then
|
|
||||||
export OVERRIDE_JAVA_VERSION="${{ matrix.java }}"
|
|
||||||
fi
|
|
||||||
./reproducible-builds/build.sh
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: signal-cli-archive-${{ matrix.java }}
|
|
||||||
path: dist/*
|
|
||||||
|
|
||||||
build-client:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- ubuntu
|
|
||||||
- macos
|
|
||||||
- windows
|
|
||||||
runs-on: ${{ matrix.os }}-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./client
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Install rust
|
|
||||||
run: rustup default stable
|
|
||||||
- name: Build client
|
|
||||||
run: cargo build --release --verbose
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: signal-cli-client-${{ matrix.os }}
|
|
||||||
path: |
|
|
||||||
client/target/release/signal-cli-client
|
|
||||||
client/target/release/signal-cli-client.exe
|
|
||||||
96
.github/workflows/ci.yml
vendored
Normal file
96
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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: [ '25', '26' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v5
|
||||||
|
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@v7
|
||||||
|
with:
|
||||||
|
name: signal-cli-archive-${{ matrix.java }}
|
||||||
|
path: build/distributions/signal-cli-*.tar.gz
|
||||||
|
|
||||||
|
build-graalvm:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: graalvm/setup-graalvm@v1
|
||||||
|
with:
|
||||||
|
distribution: 'graalvm'
|
||||||
|
java-version: '25'
|
||||||
|
cache: 'gradle'
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew --no-daemon nativeCompile
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
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@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
|
||||||
27
.github/workflows/dependency-graph.yml
vendored
27
.github/workflows/dependency-graph.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
name: dependency-graph
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
submit-dependency-graph:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '25'
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v5
|
|
||||||
with:
|
|
||||||
dependency-graph: generate-and-submit
|
|
||||||
- name: Generate dependency graph
|
|
||||||
run: ./gradlew --no-daemon dependencies
|
|
||||||
241
.github/workflows/release.yml
vendored
241
.github/workflows/release.yml
vendored
@ -5,54 +5,182 @@ 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
|
|
||||||
|
|
||||||
release:
|
ci_wf:
|
||||||
needs: build
|
permissions:
|
||||||
|
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:
|
||||||
version: ${{ steps.version.outputs.version }}
|
signal_cli_version: ${{ steps.cli_ver.outputs.version }}
|
||||||
|
release_id: ${{ steps.create_release.outputs.id }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
- name: Get signal-cli version
|
- name: Get signal-cli version
|
||||||
id: version
|
id: cli_ver
|
||||||
run: |
|
run: |
|
||||||
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
ver="${GITHUB_REF_NAME#v}"
|
||||||
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
echo "version=${ver}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create draft release and upload assets
|
- name: Extract archive
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
gh release create "v${{ steps.version.outputs.version }}" \
|
tree .
|
||||||
--repo "${{ github.repository }}" \
|
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
||||||
--title "v${{ steps.version.outputs.version }}" \
|
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||||
--draft \
|
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
|
||||||
--verify-tag \
|
rm -rf signal-cli-archive-*/
|
||||||
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
|
|
||||||
"signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \
|
# - name: Get signal-client jar version
|
||||||
"signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \
|
# id: lib_ver
|
||||||
"signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz"
|
# run: |
|
||||||
|
# JAR_PREFIX=libsignal-client-
|
||||||
|
# jar_file=$(find ./signal-cli-*/lib/ -name "$JAR_PREFIX*.jar")
|
||||||
|
# jar_version=$(echo "$jar_file" | xargs basename | sed "s/$JAR_PREFIX//; s/.jar//")
|
||||||
|
# echo "$jar_version"
|
||||||
|
# echo "signal_client_version=${jar_version}" >> $GITHUB_OUTPUT
|
||||||
|
#
|
||||||
|
# - name: Download signal-client builds
|
||||||
|
# env:
|
||||||
|
# RELEASES_URL: https://github.com/signalapp/libsignal/releases/download/
|
||||||
|
# FILE_NAMES: signal_jni.dll libsignal_jni.dylib
|
||||||
|
# SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
|
||||||
|
# run: |
|
||||||
|
# for file_name in $FILE_NAMES; do
|
||||||
|
# curl -sOL "${RELEASES_URL}/v${SIGNAL_CLIENT_VER}/${file_name}" # note: added v
|
||||||
|
# done
|
||||||
|
# tree .
|
||||||
|
|
||||||
|
- name: Compress native app
|
||||||
|
env:
|
||||||
|
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
||||||
|
run: |
|
||||||
|
chmod +x signal-cli-native/signal-cli
|
||||||
|
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-native.tar.gz -C signal-cli-native signal-cli
|
||||||
|
rm -rf signal-cli-native/
|
||||||
|
|
||||||
|
- name: Compress client app
|
||||||
|
env:
|
||||||
|
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
||||||
|
run: |
|
||||||
|
chmod +x signal-cli-client-ubuntu/signal-cli-client
|
||||||
|
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-client.tar.gz -C signal-cli-client-ubuntu signal-cli-client
|
||||||
|
rm -rf signal-cli-client-ubuntu/
|
||||||
|
|
||||||
|
# - 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 Linux client 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-client.tar.gz
|
||||||
|
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-client.tar.gz
|
||||||
|
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||||
|
|
||||||
|
# - name: Upload windows archive
|
||||||
|
# uses: actions/upload-release-asset@v1
|
||||||
|
# env:
|
||||||
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# with:
|
||||||
|
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
|
||||||
|
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
|
||||||
|
# asset_content_type: application/x-compressed-tar # .tar.gz
|
||||||
|
#
|
||||||
|
# - name: Upload macos archive
|
||||||
|
# uses: actions/upload-release-asset@v1
|
||||||
|
# env:
|
||||||
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# with:
|
||||||
|
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
|
||||||
|
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
|
||||||
|
# asset_content_type: application/x-compressed-tar # .tar.gz
|
||||||
|
|
||||||
build-container:
|
build-container:
|
||||||
needs: release
|
needs: ci_wf
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -60,23 +188,32 @@ jobs:
|
|||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@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 }}.tar.gz
|
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
||||||
|
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||||
|
rm -r signal-cli-archive-* signal-cli-native
|
||||||
mkdir -p build/install/
|
mkdir -p build/install/
|
||||||
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
|
mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v3
|
uses: redhat-actions/buildah-build@v2
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
|
||||||
containerfiles: ./Containerfile
|
containerfiles:
|
||||||
|
./Containerfile
|
||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v3
|
uses: redhat-actions/push-to-registry@v2
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
@ -90,9 +227,10 @@ jobs:
|
|||||||
echo "${{ toJSON(steps.push.outputs) }}"
|
echo "${{ toJSON(steps.push.outputs) }}"
|
||||||
|
|
||||||
build-container-native:
|
build-container-native:
|
||||||
needs: release
|
needs: ci_wf
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -100,24 +238,30 @@ jobs:
|
|||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@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/
|
||||||
mv signal-cli build/native/nativeCompile/
|
chmod +x ./signal-cli-native/signal-cli
|
||||||
chmod +x build/native/nativeCompile/signal-cli
|
mv ./signal-cli-native/signal-cli build/native/nativeCompile/
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v3
|
uses: redhat-actions/buildah-build@v2
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
|
||||||
containerfiles: ./native.Containerfile
|
containerfiles:
|
||||||
|
./native.Containerfile
|
||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v3
|
uses: redhat-actions/push-to-registry@v2
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
@ -131,9 +275,10 @@ jobs:
|
|||||||
echo "${{ toJSON(steps.push.outputs) }}"
|
echo "${{ toJSON(steps.push.outputs) }}"
|
||||||
|
|
||||||
build-container-client:
|
build-container-client:
|
||||||
needs: release
|
needs: ci_wf
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -141,24 +286,30 @@ jobs:
|
|||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@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-client.tar.gz
|
|
||||||
mkdir -p client/target/release/
|
mkdir -p client/target/release/
|
||||||
mv signal-cli-client client/target/release/
|
chmod +x ./signal-cli-client-ubuntu/signal-cli-client
|
||||||
chmod +x client/target/release/signal-cli-client
|
mv ./signal-cli-client-ubuntu/signal-cli-client client/target/release/
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v3
|
uses: redhat-actions/buildah-build@v2
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
tags: latest-client ${{ github.sha }}-client ${{ steps.cli_ver.outputs.version }}-client
|
||||||
containerfiles: ./client.Containerfile
|
containerfiles:
|
||||||
|
./client.Containerfile
|
||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v3
|
uses: redhat-actions/push-to-registry@v2
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
.gradle/
|
.gradle/
|
||||||
.kotlin/
|
|
||||||
.idea/*
|
.idea/*
|
||||||
!.idea/codeStyles/
|
!.idea/codeStyles/
|
||||||
build/
|
build/
|
||||||
@ -14,10 +13,3 @@ out/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/bin/
|
/bin/
|
||||||
/test-config/
|
/test-config/
|
||||||
/dist/
|
|
||||||
/github/
|
|
||||||
man/*.1
|
|
||||||
man/*.5
|
|
||||||
man/man1
|
|
||||||
man/man5
|
|
||||||
.superpowers/
|
|
||||||
|
|||||||
79
CHANGELOG.md
79
CHANGELOG.md
@ -1,84 +1,5 @@
|
|||||||
# 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
|
## [0.14.2] - 2026-04-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
24
README.md
24
README.md
@ -3,7 +3,7 @@
|
|||||||
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
||||||
It supports registering, verifying, sending and receiving messages.
|
It supports registering, verifying, sending and receiving messages.
|
||||||
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
||||||
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/lib/libsignal-service).
|
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal-service).
|
||||||
For registering you need a phone number where you can receive SMS or incoming calls.
|
For registering you need a phone number where you can receive SMS or incoming calls.
|
||||||
|
|
||||||
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
||||||
@ -64,20 +64,8 @@ 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:
|
||||||
@ -160,16 +148,6 @@ 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,16 +1,14 @@
|
|||||||
import groovy.json.JsonOutput
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
application
|
application
|
||||||
eclipse
|
eclipse
|
||||||
`check-lib-versions`
|
`check-lib-versions`
|
||||||
id("org.graalvm.buildtools.native") version "1.1.9"
|
id("org.graalvm.buildtools.native") version "1.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "org.asamk"
|
group = "org.asamk"
|
||||||
version = "0.14.8"
|
version = "0.14.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@ -74,11 +72,6 @@ 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")
|
||||||
@ -89,8 +82,6 @@ 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)
|
||||||
@ -99,10 +90,6 @@ dependencies {
|
|||||||
implementation(libs.slf4j.jul)
|
implementation(libs.slf4j.jul)
|
||||||
implementation(libs.logback)
|
implementation(libs.logback)
|
||||||
implementation(libs.zxing)
|
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(libs.junit.jupiter)
|
||||||
@ -173,30 +160,3 @@ tasks.register("writeLibsignalVersion") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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_25)
|
compilerOptions.jvmTarget.set(JvmTarget.JVM_24)
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
targetCompatibility = JavaVersion.VERSION_25
|
targetCompatibility = JavaVersion.VERSION_24
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
|||||||
327
client/Cargo.lock
generated
327
client/Cargo.lock
generated
@ -4,9 +4,9 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "1.0.0"
|
version = "0.6.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"anstyle-parse",
|
"anstyle-parse",
|
||||||
@ -19,15 +19,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.14"
|
version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-parse"
|
name = "anstyle-parse"
|
||||||
version = "1.0.0"
|
version = "0.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"utf8parse",
|
"utf8parse",
|
||||||
]
|
]
|
||||||
@ -83,9 +83,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.1"
|
version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
@ -95,9 +95,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.60"
|
version = "1.2.56"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@ -117,9 +117,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.1"
|
version = "4.5.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@ -127,9 +127,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.6.0"
|
version = "4.5.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@ -140,9 +140,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.6.1"
|
version = "4.5.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -152,15 +152,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "1.1.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.5"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
@ -326,9 +326,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.0"
|
version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@ -377,9 +377,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.9.0"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -391,6 +391,7 @@ dependencies = [
|
|||||||
"httparse",
|
"httparse",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
"want",
|
"want",
|
||||||
@ -398,15 +399,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.27.9"
|
version = "0.27.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"log",
|
"log",
|
||||||
"rustls",
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@ -434,13 +436,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.2.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"potential_utf",
|
"potential_utf",
|
||||||
"utf8_iter",
|
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
@ -448,9 +449,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_locale_core"
|
name = "icu_locale_core"
|
||||||
version = "2.2.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"litemap",
|
"litemap",
|
||||||
@ -461,9 +462,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer"
|
name = "icu_normalizer"
|
||||||
version = "2.2.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_normalizer_data",
|
"icu_normalizer_data",
|
||||||
@ -475,15 +476,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer_data"
|
name = "icu_normalizer_data"
|
||||||
version = "2.2.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
version = "2.2.0"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
@ -495,15 +496,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "2.2.0"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
version = "2.2.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
@ -537,9 +538,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
@ -553,9 +554,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
@ -566,7 +567,7 @@ dependencies = [
|
|||||||
"cesu8",
|
"cesu8",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"combine",
|
"combine",
|
||||||
"jni-sys 0.3.1",
|
"jni-sys",
|
||||||
"log",
|
"log",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@ -575,31 +576,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni-sys"
|
name = "jni-sys"
|
||||||
version = "0.3.1"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
dependencies = [
|
|
||||||
"jni-sys 0.4.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni-sys"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
|
||||||
dependencies = [
|
|
||||||
"jni-sys-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni-sys-macros"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonrpsee"
|
name = "jsonrpsee"
|
||||||
@ -689,9 +668,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.185"
|
version = "0.2.182"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
@ -701,9 +680,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.2"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
@ -719,9 +698,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
@ -730,9 +709,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
@ -779,19 +758,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "pin-utils"
|
||||||
version = "0.1.5"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "potential_utf"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
@ -807,9 +792,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@ -830,9 +815,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.2"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
@ -849,9 +834,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.38"
|
version = "0.23.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
|
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -912,9 +897,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.13"
|
version = "0.103.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@ -932,9 +917,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.29"
|
version = "0.1.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
@ -1041,12 +1026,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.3"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1097,12 +1082,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.4.4"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1147,9 +1132,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.3"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
@ -1157,9 +1142,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.0"
|
version = "1.49.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776"
|
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@ -1172,9 +1157,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.7.0"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1217,18 +1202,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "0.7.5+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.11+spec-1.1.0"
|
version = "0.23.10+spec-1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
|
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
@ -1238,9 +1223,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.0.9+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
@ -1375,14 +1360,14 @@ version = "0.26.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
|
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webpki-root-certs 1.0.7",
|
"webpki-root-certs 1.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.7"
|
version = "1.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@ -1429,6 +1414,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.53.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@ -1462,13 +1456,30 @@ dependencies = [
|
|||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm 0.52.6",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
"windows_aarch64_gnullvm 0.53.1",
|
||||||
|
"windows_aarch64_msvc 0.53.1",
|
||||||
|
"windows_i686_gnu 0.53.1",
|
||||||
|
"windows_i686_gnullvm 0.53.1",
|
||||||
|
"windows_i686_msvc 0.53.1",
|
||||||
|
"windows_x86_64_gnu 0.53.1",
|
||||||
|
"windows_x86_64_gnullvm 0.53.1",
|
||||||
|
"windows_x86_64_msvc 0.53.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@ -1481,6 +1492,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@ -1493,6 +1510,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@ -1505,12 +1528,24 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@ -1523,6 +1558,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@ -1535,6 +1576,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@ -1547,6 +1594,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@ -1560,25 +1613,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "windows_x86_64_msvc"
|
||||||
version = "1.0.1"
|
version = "0.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.3"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.2"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"yoke-derive",
|
"yoke-derive",
|
||||||
@ -1587,9 +1646,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke-derive"
|
name = "yoke-derive"
|
||||||
version = "0.8.2"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1599,18 +1658,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.7"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerofrom-derive",
|
"zerofrom-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom-derive"
|
name = "zerofrom-derive"
|
||||||
version = "0.1.7"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1626,9 +1685,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.4"
|
version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"yoke",
|
"yoke",
|
||||||
@ -1637,9 +1696,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec"
|
name = "zerovec"
|
||||||
version = "0.11.6"
|
version = "0.11.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
@ -1648,9 +1707,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec-derive"
|
name = "zerovec-derive"
|
||||||
version = "0.11.3"
|
version = "0.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@ -45,21 +45,6 @@
|
|||||||
<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">
|
<release version="0.14.2" date="2026-04-04">
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
@ -1,29 +1,20 @@
|
|||||||
[versions]
|
[versions]
|
||||||
slf4j = "2.0.18"
|
slf4j = "2.0.17"
|
||||||
coroutines = "1.10.2"
|
junit = "6.0.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.85"
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83"
|
||||||
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.1"
|
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.20.2"
|
||||||
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"
|
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.6.1"
|
logback = "ch.qos.logback:logback-classic:1.5.32"
|
||||||
|
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_142"
|
||||||
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
|
sqlite = "org.xerial:sqlite-jdbc:3.51.2.0"
|
||||||
sqlite = "org.xerial:sqlite-jdbc:3.53.2.1"
|
hikari = "com.zaxxer:HikariCP:7.0.2"
|
||||||
hikari = "com.zaxxer:HikariCP:7.1.0"
|
|
||||||
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
|
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
|
||||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", 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" }
|
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,9 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.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 @@
|
|||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# gradlew start up script for POSIX generated by Gradle.
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
#
|
#
|
||||||
# Important for running:
|
# Important for running:
|
||||||
#
|
#
|
||||||
@ -29,7 +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 gradlew
|
# ksh Gradle
|
||||||
#
|
#
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
@ -57,7 +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/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/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 gradlew startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
@rem Set local scope for the variables with windows NT shell
|
||||||
setlocal EnableExtensions
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@ -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
|
||||||
|
|
||||||
"%COMSPEC%" /c exit 1
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@ -65,18 +65,29 @@ 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
|
||||||
|
|
||||||
"%COMSPEC%" /c exit 1
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute gradlew
|
@rem Execute Gradle
|
||||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
@rem which allows us to clear the local environment before executing the java command
|
|
||||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
|
||||||
|
|
||||||
:exitWithErrorLevel
|
:end
|
||||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
@rem End local scope for the variables with windows NT shell
|
||||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
|
|||||||
@ -18,9 +18,9 @@ val libsignalClientPath = project.findProperty("libsignal_client_path")?.toStrin
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
if (libsignalClientPath == null) {
|
if (libsignalClientPath == null) {
|
||||||
implementation(libs.signalnetwork)
|
implementation(libs.signalservice)
|
||||||
} else {
|
} else {
|
||||||
implementation(libs.signalnetwork) {
|
implementation(libs.signalservice) {
|
||||||
exclude(group = "org.signal", module = "libsignal-client")
|
exclude(group = "org.signal", module = "libsignal-client")
|
||||||
}
|
}
|
||||||
implementation(files(libsignalClientPath))
|
implementation(files(libsignalClientPath))
|
||||||
@ -30,7 +30,6 @@ 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))
|
testImplementation(platform(libs.junit.jupiter.bom))
|
||||||
|
|||||||
@ -54,7 +54,6 @@ 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;
|
||||||
|
|
||||||
@ -75,10 +74,6 @@ 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 {
|
||||||
@ -96,8 +91,6 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
String getSelfNumber();
|
String getSelfNumber();
|
||||||
|
|
||||||
String getSelfACI();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is used for checking a set of phone numbers for registration on Signal
|
* This is used for checking a set of phone numbers for registration on Signal
|
||||||
*
|
*
|
||||||
@ -184,10 +177,6 @@ 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,
|
||||||
@ -224,19 +213,6 @@ 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
|
||||||
@ -283,7 +259,7 @@ public interface Manager extends Closeable {
|
|||||||
RecipientIdentifier.Single recipient
|
RecipientIdentifier.Single recipient
|
||||||
) throws IOException;
|
) throws IOException;
|
||||||
|
|
||||||
void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
||||||
|
|
||||||
SendMessageResults sendMessageRequestResponse(
|
SendMessageResults sendMessageRequestResponse(
|
||||||
MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
||||||
|
|||||||
@ -15,7 +15,6 @@ 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;
|
||||||
@ -68,7 +67,7 @@ public class SignalAccountFiles {
|
|||||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
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>(initManagerByNumber(a.number(), a.path()), null);
|
return new Pair<Manager, Throwable>(initManager(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;
|
||||||
@ -91,31 +90,15 @@ public class SignalAccountFiles {
|
|||||||
return new MultiAccountManagerImpl(managers, this);
|
return new MultiAccountManagerImpl(managers, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
final var accountPath = accountsStore.getPathByNumber(number);
|
final var accountPath = accountsStore.getPathByNumber(number);
|
||||||
return this.initManagerByNumber(number, accountPath);
|
return this.initManager(number, accountPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Manager initManagerByAci(String aciStr) throws IOException, NotRegisteredException, AccountCheckException {
|
private Manager initManager(
|
||||||
final var aci = ACI.parseOrThrow(aciStr);
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
@ -123,27 +106,12 @@ public class SignalAccountFiles {
|
|||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||||
}
|
if (!number.equals(account.getNumber())) {
|
||||||
|
|
||||||
private Manager initManagerByAci(
|
|
||||||
ACI aci,
|
|
||||||
String accountPath
|
|
||||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
|
||||||
final var account = loadAccount(accountPath);
|
|
||||||
if (!aci.equals(account.getAci())) {
|
|
||||||
account.close();
|
account.close();
|
||||||
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci());
|
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
return initManagerFromAccount(aci.toString(), accountPath, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ManagerImpl initManagerFromAccount(
|
|
||||||
final String identifier,
|
|
||||||
final String accountPath,
|
|
||||||
final SignalAccount account
|
|
||||||
) throws NotRegisteredException, IOException, AccountCheckException {
|
|
||||||
if (!account.isRegistered()) {
|
if (!account.isRegistered()) {
|
||||||
account.close();
|
account.close();
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
@ -168,7 +136,7 @@ public class SignalAccountFiles {
|
|||||||
throw new IOException("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 " + identifier + ": " + e.getMessage(), e);
|
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.getServiceEnvironment() == null) {
|
if (account.getServiceEnvironment() == null) {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
public record CallOffer(
|
public record CallOffer(
|
||||||
long callId, Type type, byte[] opaque
|
long callId,
|
||||||
|
Type type,
|
||||||
|
byte[] opaque
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
|
|||||||
@ -2,11 +2,11 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
public class CaptchaRequiredException extends Exception {
|
public class CaptchaRequiredException extends Exception {
|
||||||
|
|
||||||
private long nextVerificationAttemptMilliseconds;
|
private long nextAttemptTimestamp;
|
||||||
|
|
||||||
public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) {
|
public CaptchaRequiredException(final long nextAttemptTimestamp) {
|
||||||
super("Captcha required");
|
super("Captcha required");
|
||||||
this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds;
|
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CaptchaRequiredException(final String message) {
|
public CaptchaRequiredException(final String message) {
|
||||||
@ -17,7 +17,7 @@ public class CaptchaRequiredException extends Exception {
|
|||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getNextVerificationAttemptMilliseconds() {
|
public long getNextAttemptTimestamp() {
|
||||||
return nextVerificationAttemptMilliseconds;
|
return nextAttemptTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ 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,
|
||||||
@ -35,7 +34,6 @@ 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,
|
||||||
@ -60,7 +58,6 @@ 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();
|
||||||
@ -83,21 +80,6 @@ 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;
|
||||||
@ -112,7 +94,6 @@ 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;
|
||||||
@ -181,20 +162,10 @@ 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;
|
||||||
|
|||||||
@ -22,8 +22,7 @@ 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(
|
||||||
@ -60,7 +59,6 @@ public record Group(
|
|||||||
groupInfo.getPermissionEditDetails(),
|
groupInfo.getPermissionEditDetails(),
|
||||||
groupInfo.getPermissionSendMessage(),
|
groupInfo.getPermissionSendMessage(),
|
||||||
groupInfo.isMember(selfRecipientId),
|
groupInfo.isMember(selfRecipientId),
|
||||||
groupInfo.isAdmin(selfRecipientId),
|
groupInfo.isAdmin(selfRecipientId));
|
||||||
groupInfo.isTerminated());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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.asamk.signal.manager.util.MimeUtils;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
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;
|
||||||
@ -156,7 +157,7 @@ public record MessageEnvelope(
|
|||||||
dataMessage.getExpiresInSeconds(),
|
dataMessage.getExpiresInSeconds(),
|
||||||
dataMessage.isExpirationUpdate(),
|
dataMessage.isExpirationUpdate(),
|
||||||
dataMessage.isViewOnce(),
|
dataMessage.isViewOnce(),
|
||||||
false,
|
dataMessage.isEndSession(),
|
||||||
dataMessage.isProfileKeyUpdate(),
|
dataMessage.isProfileKeyUpdate(),
|
||||||
dataMessage.getProfileKey().isPresent(),
|
dataMessage.getProfileKey().isPresent(),
|
||||||
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
|
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
|
||||||
@ -267,19 +268,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,7 +599,7 @@ public record MessageEnvelope(
|
|||||||
Boolean.TRUE.equals(pinnedMessage.getForever())
|
Boolean.TRUE.equals(pinnedMessage.getForever())
|
||||||
? -1
|
? -1
|
||||||
: pinnedMessage.getPinDurationInSeconds() == null
|
: pinnedMessage.getPinDurationInSeconds() == null
|
||||||
? 0
|
? 0
|
||||||
: pinnedMessage.getPinDurationInSeconds());
|
: pinnedMessage.getPinDurationInSeconds());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -739,10 +740,7 @@ public record MessageEnvelope(
|
|||||||
null,
|
null,
|
||||||
d.getE164(),
|
d.getE164(),
|
||||||
null))
|
null))
|
||||||
.toList(),
|
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
|
||||||
blockedListMessage.groups.stream()
|
|
||||||
.map(group -> GroupId.unknownVersion(group.getGroupId()))
|
|
||||||
.toList());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1030,18 +1028,18 @@ public record MessageEnvelope(
|
|||||||
final AttachmentFileProvider fileProvider,
|
final AttachmentFileProvider fileProvider,
|
||||||
Exception exception
|
Exception exception
|
||||||
) {
|
) {
|
||||||
final var serviceId = envelope.getSourceServiceId();
|
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
|
||||||
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;
|
||||||
|
|||||||
@ -10,19 +10,12 @@ public class ProofRequiredException extends Exception {
|
|||||||
|
|
||||||
private final String token;
|
private final String token;
|
||||||
private final Set<Option> options;
|
private final Set<Option> options;
|
||||||
private final long retryAfterMilliseconds;
|
private final long retryAfterSeconds;
|
||||||
|
|
||||||
public ProofRequiredException(final String token, final Set<Option> options, final long retryAfterMilliseconds) {
|
public ProofRequiredException(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
|
||||||
super("Rate limit");
|
this.token = e.getToken();
|
||||||
this.token = token;
|
this.options = e.getOptions().stream().map(Option::from).collect(Collectors.toSet());
|
||||||
this.options = options;
|
this.retryAfterSeconds = e.getRetryAfterSeconds();
|
||||||
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ProofRequiredException from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
|
|
||||||
return new ProofRequiredException(e.getToken(),
|
|
||||||
e.getOptions().stream().map(Option::from).collect(Collectors.toSet()),
|
|
||||||
e.getRetryAfterSeconds() * 1000L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getToken() {
|
public String getToken() {
|
||||||
@ -33,8 +26,8 @@ public class ProofRequiredException extends Exception {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getRetryAfterMilliseconds() {
|
public long getRetryAfterSeconds() {
|
||||||
return retryAfterMilliseconds;
|
return retryAfterSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Option {
|
public enum Option {
|
||||||
|
|||||||
@ -2,18 +2,14 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
public class RateLimitException extends Exception {
|
public class RateLimitException extends Exception {
|
||||||
|
|
||||||
private final Long retryAfterMilliseconds;
|
private final long nextAttemptTimestamp;
|
||||||
|
|
||||||
public RateLimitException(final Long retryAfterMilliseconds) {
|
public RateLimitException(final long nextAttemptTimestamp) {
|
||||||
super("Rate limit");
|
super("Rate limit");
|
||||||
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RateLimitException from(org.whispersystems.signalservice.api.push.exceptions.RateLimitException e) {
|
public long getNextAttemptTimestamp() {
|
||||||
return new RateLimitException(e.getRetryAfterMilliseconds().orElse(null));
|
return nextAttemptTimestamp;
|
||||||
}
|
|
||||||
|
|
||||||
public Long getRetryAfterMilliseconds() {
|
|
||||||
return retryAfterMilliseconds;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,3 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
public record ReceiveConfig(
|
public record ReceiveConfig(boolean ignoreAttachments, boolean ignoreStories, boolean ignoreAvatars, boolean ignoreStickers, boolean sendReadReceipts) {}
|
||||||
boolean ignoreAttachments,
|
|
||||||
boolean ignoreStories,
|
|
||||||
boolean ignoreAvatars,
|
|
||||||
boolean ignoreStickers,
|
|
||||||
boolean sendReadReceipts
|
|
||||||
) {}
|
|
||||||
|
|||||||
@ -9,13 +9,13 @@ public record SendMessageResult(
|
|||||||
boolean isNetworkFailure,
|
boolean isNetworkFailure,
|
||||||
boolean isUnregisteredFailure,
|
boolean isUnregisteredFailure,
|
||||||
boolean isIdentityFailure,
|
boolean isIdentityFailure,
|
||||||
RateLimitException rateLimitException,
|
boolean isRateLimitFailure,
|
||||||
ProofRequiredException proofRequiredFailure,
|
ProofRequiredException proofRequiredFailure,
|
||||||
boolean isInvalidPreKeyFailure
|
boolean isInvalidPreKeyFailure
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static SendMessageResult unregisteredFailure(RecipientAddress address) {
|
public static SendMessageResult unregisteredFailure(RecipientAddress address) {
|
||||||
return new SendMessageResult(address, false, false, true, false, null, null, false);
|
return new SendMessageResult(address, false, false, true, false, false, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SendMessageResult from(
|
public static SendMessageResult from(
|
||||||
@ -23,30 +23,16 @@ public record SendMessageResult(
|
|||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver
|
RecipientAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
final var rateLimitFailure = sendMessageResult.getRateLimitFailure();
|
|
||||||
final var proofRequiredFailure = sendMessageResult.getProofRequiredFailure();
|
|
||||||
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
sendMessageResult.getAddress())).toApiRecipientAddress(),
|
sendMessageResult.getAddress())).toApiRecipientAddress(),
|
||||||
sendMessageResult.isSuccess(),
|
sendMessageResult.isSuccess(),
|
||||||
sendMessageResult.isNetworkFailure(),
|
sendMessageResult.isNetworkFailure(),
|
||||||
sendMessageResult.isUnregisteredFailure(),
|
sendMessageResult.isUnregisteredFailure(),
|
||||||
sendMessageResult.getIdentityFailure() != null,
|
sendMessageResult.getIdentityFailure() != null,
|
||||||
rateLimitFailure == null ? null : RateLimitException.from(rateLimitFailure),
|
sendMessageResult.getRateLimitFailure() != null || sendMessageResult.getProofRequiredFailure() != null,
|
||||||
proofRequiredFailure == null ? null : ProofRequiredException.from(proofRequiredFailure),
|
sendMessageResult.getProofRequiredFailure() == null
|
||||||
|
? null
|
||||||
|
: new ProofRequiredException(sendMessageResult.getProofRequiredFailure()),
|
||||||
sendMessageResult.isInvalidPreKeyFailure());
|
sendMessageResult.isInvalidPreKeyFailure());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRateLimitFailure() {
|
|
||||||
return this.rateLimitException != null || this.proofRequiredFailure != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long rateLimitRetryAfterMilliseconds() {
|
|
||||||
if (proofRequiredFailure != null) {
|
|
||||||
return proofRequiredFailure.getRetryAfterMilliseconds();
|
|
||||||
} else if (rateLimitException != null) {
|
|
||||||
return rateLimitException.getRetryAfterMilliseconds();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
|
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
|
||||||
|
|
||||||
@ -27,18 +26,4 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,5 +3,8 @@ package org.asamk.signal.manager.api;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record TurnServer(
|
public record TurnServer(
|
||||||
String username, String password, List<String> urls
|
String username,
|
||||||
) {}
|
String password,
|
||||||
|
List<String> urls
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|||||||
@ -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.signal.network.config.HttpProxy;
|
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||||
import org.signal.network.config.SignalCdnUrl;
|
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||||
import org.signal.network.config.SignalCdsiUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||||
import org.signal.network.config.SignalProxy;
|
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||||
import org.signal.network.config.SignalServiceConfiguration;
|
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||||
import org.signal.network.config.SignalServiceUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||||
import org.signal.network.config.SignalStorageUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
||||||
import org.signal.network.config.SignalSvr2Url;
|
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
||||||
import org.signal.network.config.TrustStore;
|
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -30,8 +30,7 @@ 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 = "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";
|
||||||
@ -94,7 +93,7 @@ class LiveConfig {
|
|||||||
createDefaultServiceConfiguration(interceptors),
|
createDefaultServiceConfiguration(interceptors),
|
||||||
getUnidentifiedSenderTrustRoots(),
|
getUnidentifiedSenderTrustRoots(),
|
||||||
CDSI_MRENCLAVE,
|
CDSI_MRENCLAVE,
|
||||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
List.of(SVR2_MRENCLAVE));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LiveConfig() {
|
private LiveConfig() {
|
||||||
|
|||||||
@ -31,14 +31,7 @@ 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 = true;
|
final var spqr = true;
|
||||||
final var usernameSyncChangeMessage = !isPrimaryDevice;
|
return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr);
|
||||||
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.signal.network.config.SignalServiceConfiguration;
|
import org.whispersystems.signalservice.internal.configuration.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.signal.network.config.HttpProxy;
|
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||||
import org.signal.network.config.SignalCdnUrl;
|
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||||
import org.signal.network.config.SignalCdsiUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||||
import org.signal.network.config.SignalProxy;
|
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||||
import org.signal.network.config.SignalServiceConfiguration;
|
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||||
import org.signal.network.config.SignalServiceUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||||
import org.signal.network.config.SignalStorageUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
||||||
import org.signal.network.config.SignalSvr2Url;
|
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
||||||
import org.signal.network.config.TrustStore;
|
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -30,8 +30,7 @@ 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 = "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";
|
||||||
@ -94,7 +93,7 @@ class StagingConfig {
|
|||||||
createDefaultServiceConfiguration(interceptors),
|
createDefaultServiceConfiguration(interceptors),
|
||||||
getUnidentifiedSenderTrustRoots(),
|
getUnidentifiedSenderTrustRoots(),
|
||||||
CDSI_MRENCLAVE,
|
CDSI_MRENCLAVE,
|
||||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
List.of(SVR2_MRENCLAVE));
|
||||||
}
|
}
|
||||||
|
|
||||||
private StagingConfig() {
|
private StagingConfig() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.asamk.signal.manager.config;
|
package org.asamk.signal.manager.config;
|
||||||
|
|
||||||
import org.signal.network.config.TrustStore;
|
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
|||||||
@ -17,23 +17,19 @@ import org.asamk.signal.manager.util.Utils;
|
|||||||
import org.signal.core.models.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
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.messages.multidevice.DeviceInfo;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||||
@ -41,6 +37,8 @@ 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.NonSuccessfulResponseCodeException;
|
||||||
|
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,9 +46,7 @@ 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;
|
||||||
@ -61,7 +57,6 @@ 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 {
|
||||||
@ -285,7 +280,7 @@ 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 | NoSessionException e) {
|
} catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,14 +320,10 @@ public class AccountHelper {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePniChangeNumberMessage(selfChangeNumber, updatePni, false);
|
handlePniChangeNumberMessage(selfChangeNumber, updatePni);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handlePniChangeNumberMessage(
|
public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
|
||||||
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) {
|
||||||
@ -346,19 +337,10 @@ 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;
|
||||||
@ -400,17 +382,13 @@ 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<byte[]>();
|
final var candidateHashes = new ArrayList<String>();
|
||||||
for (final var candidate : candidates) {
|
for (final var candidate : candidates) {
|
||||||
candidateHashes.add(candidate.getHash());
|
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
|
||||||
}
|
}
|
||||||
|
|
||||||
final var usernameHash = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||||
final var hashIndex = candidateHashes.stream()
|
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
||||||
.filter(candidateHash -> Arrays.equals(candidateHash, usernameHash))
|
|
||||||
.findFirst()
|
|
||||||
.map(candidateHashes::indexOf)
|
|
||||||
.orElse(-1);
|
|
||||||
if (hashIndex == -1) {
|
if (hashIndex == -1) {
|
||||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||||
throw new IOException("Unexpected username response");
|
throw new IOException("Unexpected username response");
|
||||||
@ -503,7 +481,8 @@ public class AccountHelper {
|
|||||||
final var usernameLink = account.getUsernameLink();
|
final var usernameLink = account.getUsernameLink();
|
||||||
|
|
||||||
if (usernameLink == null) {
|
if (usernameLink == null) {
|
||||||
handleResponseException(dependencies.getAccountApi().reserveUsername(List.of(username.getHash())));
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.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);
|
||||||
@ -528,34 +507,28 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void deleteUsername() throws IOException {
|
public void deleteUsername() throws IOException {
|
||||||
handleResponseException(dependencies.getAccountApi().deleteUsernameHash());
|
handleResponseException(dependencies.getAccountApi().deleteUsername());
|
||||||
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 encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
account.setEncryptedDeviceName(Base64.encodeWithoutPadding(encryptedDeviceName));
|
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
||||||
|
account.setEncryptedDeviceName(encryptedDeviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
||||||
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
||||||
.setDeviceName(encryptedDeviceName, deviceId, cont));
|
handleResponseException(dependencies.getLinkDeviceApi().setDeviceName(encryptedDeviceName, deviceId));
|
||||||
context.getSyncHelper().sendDeviceNameChange(deviceId);
|
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 {
|
public void refreshDeviceName() throws IOException {
|
||||||
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
final var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||||
.getDevices(cont));
|
|
||||||
final var deviceId = account.getDeviceId();
|
final var deviceId = account.getDeviceId();
|
||||||
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
|
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
|
||||||
if (device.isPresent()) {
|
if (device.isPresent()) {
|
||||||
@ -589,16 +562,14 @@ 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 {
|
||||||
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi().removeDevice(deviceId, cont));
|
handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
|
||||||
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||||
.getDevices(cont));
|
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -606,14 +577,16 @@ public class AccountHelper {
|
|||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
||||||
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.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().enableRegistrationLock(masterKey));
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||||
|
|
||||||
account.setRegistrationLockPin(pin);
|
account.setRegistrationLockPin(pin);
|
||||||
updateAccountAttributes();
|
updateAccountAttributes();
|
||||||
@ -631,7 +604,7 @@ public class AccountHelper {
|
|||||||
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
||||||
// If this is the primary device, other users can't send messages to this number anymore.
|
// If this is the primary device, other users can't send messages to this number anymore.
|
||||||
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
||||||
handleResponseExceptionSuspend(cont -> dependencies.getAccountApi().clearFcmToken(cont));
|
handleResponseException(dependencies.getAccountApi().clearFcmToken());
|
||||||
|
|
||||||
account.setRegistered(false);
|
account.setRegistered(false);
|
||||||
unregisteredListener.call();
|
unregisteredListener.call();
|
||||||
|
|||||||
@ -6,19 +6,15 @@ 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;
|
||||||
@ -34,10 +30,8 @@ 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();
|
||||||
}
|
}
|
||||||
@ -50,10 +44,7 @@ public class AttachmentHelper {
|
|||||||
return attachmentStore.retrieveAttachment(id);
|
return attachmentStore.retrieveAttachment(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SignalServiceAttachment> uploadAttachments(
|
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments, boolean voiceNote) throws AttachmentInvalidException, IOException {
|
||||||
final List<String> attachments,
|
|
||||||
boolean voiceNote
|
|
||||||
) throws AttachmentInvalidException, IOException {
|
|
||||||
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
|
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -74,63 +65,21 @@ public class AttachmentHelper {
|
|||||||
return uploadAttachments(attachments, false);
|
return uploadAttachments(attachments, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SignalServiceAttachmentStream> createAttachmentStreams(
|
private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments, boolean voiceNote) throws AttachmentInvalidException, IOException {
|
||||||
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 attachmentStream = getAttachmentStream(attachment, voiceNote);
|
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||||
signalServiceAttachments.add(attachmentStream);
|
signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, voiceNote, uploadSpec));
|
||||||
}
|
}
|
||||||
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 attachmentStream = getAttachmentStream(attachment, false);
|
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||||
|
var attachmentStream = AttachmentUtils.createAttachmentStream(attachment, uploadSpec);
|
||||||
return uploadAttachment(attachmentStream);
|
return uploadAttachment(attachmentStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -105,8 +105,6 @@ public class CallManager implements AutoCloseable {
|
|||||||
recipientAddress,
|
recipientAddress,
|
||||||
recipientId);
|
recipientId);
|
||||||
activeCalls.put(callId, state);
|
activeCalls.put(callId, state);
|
||||||
dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
|
|
||||||
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
|
|
||||||
fireCallEvent(state, null);
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
// Spawn call tunnel binary and connect control channel
|
// Spawn call tunnel binary and connect control channel
|
||||||
@ -199,6 +197,11 @@ public class CallManager implements AutoCloseable {
|
|||||||
if (callEventListeners.isEmpty()) {
|
if (callEventListeners.isEmpty()) {
|
||||||
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
|
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
|
||||||
callIdUnsigned(callId));
|
callIdUnsigned(callId));
|
||||||
|
|
||||||
|
final var result = sendBusyMessage(callId, recipientId, deviceId);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
logger.warn("Failed to send busy for unhandled call {}", callIdUnsigned(callId));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,7 +652,7 @@ public class CallManager implements AutoCloseable {
|
|||||||
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
|
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
|
||||||
default -> HangupMessage.Type.NORMAL;
|
default -> HangupMessage.Type.NORMAL;
|
||||||
};
|
};
|
||||||
var hangupMessage = new HangupMessage(state.callId, type, 0);
|
var hangupMessage = new HangupMessage(state.callId, type, state.deviceId);
|
||||||
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
|
||||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
|
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
|
||||||
@ -698,8 +701,6 @@ public class CallManager implements AutoCloseable {
|
|||||||
|
|
||||||
private void endCall(final long callId, final String reason) {
|
private void endCall(final long callId, final String reason) {
|
||||||
var state = activeCalls.remove(callId);
|
var state = activeCalls.remove(callId);
|
||||||
dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
|
|
||||||
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
|
|
||||||
if (state == null) return;
|
if (state == null) return;
|
||||||
|
|
||||||
state.state = CallInfo.State.ENDED;
|
state.state = CallInfo.State.ENDED;
|
||||||
@ -711,7 +712,7 @@ public class CallManager implements AutoCloseable {
|
|||||||
&& !"rejected".equals(reason)
|
&& !"rejected".equals(reason)
|
||||||
&& !"remote_busy".equals(reason)
|
&& !"remote_busy".equals(reason)
|
||||||
&& !"ringrtc_hangup".equals(reason)) {
|
&& !"ringrtc_hangup".equals(reason)) {
|
||||||
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, 0);
|
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, state.deviceId);
|
||||||
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
||||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
|
|||||||
@ -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,18 +85,12 @@ 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()
|
account.getContactStore().storeContact(recipientId, builder.withIsBlocked(blocked).build());
|
||||||
.storeContact(recipientId,
|
|
||||||
builder.withIsBlocked(blocked).withBlockedAt(blocked ? blockedAt : 0).build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
||||||
|
|||||||
@ -123,7 +123,7 @@ public class GroupHelper {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||||
return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty(), uploadSpec));
|
return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty(), uploadSpec));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,28 +363,6 @@ 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);
|
||||||
|
|
||||||
@ -459,21 +437,12 @@ 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());
|
||||||
}
|
}
|
||||||
@ -589,24 +558,16 @@ 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);
|
||||||
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
|
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
|
||||||
}
|
final var profileStore = account.getProfileStore();
|
||||||
for (var member : group.requestingMembers) {
|
if (profileStore.getProfileKey(recipientId) != null) {
|
||||||
final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
|
// We already have a profile key, not updating it from a non-authoritative source
|
||||||
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
|
continue;
|
||||||
}
|
}
|
||||||
}
|
try {
|
||||||
|
profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray()));
|
||||||
private void storeProfileKeyIfMissing(final ServiceId serviceId, final byte[] profileKeyBytes) {
|
} catch (InvalidInputException ignored) {
|
||||||
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) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -765,7 +726,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.getMemberRecipientIds());
|
newMembers.removeAll(group.getMembers());
|
||||||
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);
|
||||||
@ -807,8 +768,12 @@ public class GroupHelper {
|
|||||||
newAdmins.retainAll(group.getMemberRecipientIds());
|
newAdmins.retainAll(group.getMemberRecipientIds());
|
||||||
newAdmins.removeAll(group.getAdminMemberRecipientIds());
|
newAdmins.removeAll(group.getAdminMemberRecipientIds());
|
||||||
if (!newAdmins.isEmpty()) {
|
if (!newAdmins.isEmpty()) {
|
||||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, newAdmins, true);
|
for (var admin : newAdmins) {
|
||||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
|
||||||
|
result = sendUpdateGroupV2Message(group,
|
||||||
|
groupGroupChangePair.first(),
|
||||||
|
groupGroupChangePair.second());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -816,8 +781,12 @@ public class GroupHelper {
|
|||||||
final var existingRemoveAdmins = new HashSet<>(removeAdmins);
|
final var existingRemoveAdmins = new HashSet<>(removeAdmins);
|
||||||
existingRemoveAdmins.retainAll(group.getAdminMemberRecipientIds());
|
existingRemoveAdmins.retainAll(group.getAdminMemberRecipientIds());
|
||||||
if (!existingRemoveAdmins.isEmpty()) {
|
if (!existingRemoveAdmins.isEmpty()) {
|
||||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, existingRemoveAdmins, false);
|
for (var admin : existingRemoveAdmins) {
|
||||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
|
||||||
|
result = sendUpdateGroupV2Message(group,
|
||||||
|
groupGroupChangePair.first(),
|
||||||
|
groupGroupChangePair.second());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,6 @@ 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.network.exceptions.NonSuccessfulResponseCodeException;
|
|
||||||
import org.signal.storageservice.storage.protos.groups.AccessControl;
|
import org.signal.storageservice.storage.protos.groups.AccessControl;
|
||||||
import org.signal.storageservice.storage.protos.groups.GroupChange;
|
import org.signal.storageservice.storage.protos.groups.GroupChange;
|
||||||
import org.signal.storageservice.storage.protos.groups.GroupChangeResponse;
|
import org.signal.storageservice.storage.protos.groups.GroupChangeResponse;
|
||||||
@ -44,6 +43,7 @@ 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.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -264,9 +264,6 @@ 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()));
|
||||||
@ -504,25 +501,18 @@ class GroupV2Helper {
|
|||||||
|
|
||||||
Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
|
Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
|
||||||
GroupInfoV2 groupInfoV2,
|
GroupInfoV2 groupInfoV2,
|
||||||
Set<RecipientId> recipientIds,
|
RecipientId recipientId,
|
||||||
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;
|
||||||
final var change = new GroupChange.Actions.Builder();
|
if (address.getServiceId() instanceof ACI aci) {
|
||||||
final var memberRoles = recipientIds.stream()
|
final var change = groupOperations.createChangeMemberRole(aci, newRole);
|
||||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
return commitChange(groupInfoV2, change);
|
||||||
.map(SignalServiceAddress::getServiceId)
|
} else {
|
||||||
.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(
|
||||||
@ -543,14 +533,6 @@ 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(
|
Pair<DecryptedGroup, GroupChangeResponse> setMemberLabels(
|
||||||
GroupInfoV2 groupInfoV2,
|
GroupInfoV2 groupInfoV2,
|
||||||
String labelEmoji,
|
String labelEmoji,
|
||||||
|
|||||||
@ -109,8 +109,8 @@ 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());
|
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||||
@ -140,30 +140,15 @@ 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;
|
||||||
if (envelope.getSourceServiceId() != null) {
|
envelope.getSourceServiceId().map(ServiceId::parseOrNull)
|
||||||
// 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
|
||||||
account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId());
|
.ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId));
|
||||||
}
|
|
||||||
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());
|
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||||
@ -188,13 +173,7 @@ 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());
|
||||||
ServiceId destination;
|
if (serviceId != null) {
|
||||||
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.");
|
||||||
@ -332,17 +311,12 @@ public final class IncomingMessageHandler {
|
|||||||
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 DeviceAddress destination;
|
final var destination = getDestination(envelope);
|
||||||
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(sender, true);
|
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true);
|
||||||
} else if (account.getAci().equals(destination.serviceId)) {
|
} else if (account.getAci().equals(destination.serviceId)) {
|
||||||
account.getRecipientStore().markNeedsPniSignature(sender, false);
|
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.getReceiptMessage().isPresent()) {
|
if (content.getReceiptMessage().isPresent()) {
|
||||||
@ -498,11 +472,10 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
|
logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
|
||||||
final var recipientId = account.getRecipientTrustedResolver()
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -626,12 +599,13 @@ 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, individual.getBlockedAt());
|
context.getContactHelper().setContactBlocked(recipientId, true);
|
||||||
}
|
}
|
||||||
for (var group : blockedListMessage.groups) {
|
for (var groupId : blockedListMessage.groupIds.stream()
|
||||||
final var groupId = GroupId.unknownVersion(group.getGroupId());
|
.map(GroupId::unknownVersion)
|
||||||
|
.collect(Collectors.toSet())) {
|
||||||
try {
|
try {
|
||||||
context.getGroupHelper().setGroupBlocked(groupId, true, group.getBlockedAt());
|
context.getGroupHelper().setGroupBlocked(groupId, true);
|
||||||
} 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());
|
||||||
@ -717,30 +691,11 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
if (syncMessage.getPniChangeNumber().isPresent()) {
|
if (syncMessage.getPniChangeNumber().isPresent()) {
|
||||||
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
||||||
if (account.isPrimaryDevice()) {
|
logger.debug("Received PNI change number sync message, applying.");
|
||||||
logger.warn("Received PNI change number sync message on primary device, ignoring.");
|
final var updatedPniString = envelope.getUpdatedPni();
|
||||||
} else if (sender.deviceId() != SignalServiceAddress.DEFAULT_DEVICE_ID) {
|
if (updatedPniString != null && !updatedPniString.isEmpty()) {
|
||||||
logger.warn("Received PNI change number sync message from non-primary device {}, ignoring.",
|
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
|
||||||
sender.deviceId());
|
context.getAccountHelper().handlePniChangeNumberMessage(pniChangeNumber, updatedPni);
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (syncMessage.getDeviceNameChange().isPresent()) {
|
if (syncMessage.getDeviceNameChange().isPresent()) {
|
||||||
@ -836,15 +791,6 @@ public final class IncomingMessageHandler {
|
|||||||
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()
|
||||||
@ -928,6 +874,11 @@ public final class IncomingMessageHandler {
|
|||||||
|
|
||||||
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();
|
||||||
@ -1096,7 +1047,7 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||||
final var serviceId = envelope.getSourceServiceId();
|
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
|
||||||
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) {
|
||||||
@ -1107,7 +1058,7 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||||
final var serviceId = envelope.getSourceServiceId();
|
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
|
||||||
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,
|
||||||
@ -1119,13 +1070,10 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DeviceAddress getDestination(SignalServiceEnvelope envelope) throws InvalidMessageException {
|
private DeviceAddress getDestination(SignalServiceEnvelope envelope) {
|
||||||
final var destination = envelope.getDestinationServiceId();
|
final var destination = envelope.getDestinationServiceId();
|
||||||
if (destination == null || destination.isUnknown()) {
|
if (destination == null || destination.isUnknown()) {
|
||||||
throw new InvalidMessageException("Missing destination");
|
return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
|
||||||
}
|
|
||||||
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,
|
||||||
|
|||||||
@ -9,7 +9,6 @@ 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;
|
||||||
@ -17,6 +16,7 @@ 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,8 +84,7 @@ public class PreKeyHelper {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
OneTimePreKeyCounts preKeyCounts;
|
OneTimePreKeyCounts preKeyCounts;
|
||||||
try {
|
try {
|
||||||
preKeyCounts = handleResponseException(dependencies.getKeysApi()
|
preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
|
||||||
.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);
|
||||||
@ -146,7 +145,7 @@ public class PreKeyHelper {
|
|||||||
kyberPreKeyRecords);
|
kyberPreKeyRecords);
|
||||||
var needsReset = false;
|
var needsReset = false;
|
||||||
try {
|
try {
|
||||||
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeysSync(preKeyUpload));
|
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
|
||||||
try {
|
try {
|
||||||
if (preKeyRecords != null) {
|
if (preKeyRecords != null) {
|
||||||
account.addPreKeys(serviceIdType, preKeyRecords);
|
account.addPreKeys(serviceIdType, preKeyRecords);
|
||||||
|
|||||||
@ -17,12 +17,10 @@ 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;
|
||||||
@ -32,7 +30,9 @@ 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;
|
||||||
@ -134,9 +134,7 @@ public final class ProfileHelper {
|
|||||||
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL,
|
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL,
|
||||||
false));
|
false));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warn("Failed to retrieve profile key credential for {}, ignoring: {}",
|
logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
|
||||||
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
|
||||||
e.getMessage());
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,11 +191,9 @@ 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
|
: forceUploadAvatar && avatar == null ? context.getAvatarStore()
|
||||||
? context.getAvatarStore()
|
.retrieveProfileAvatar(account.getSelfRecipientAddress()) : null;
|
||||||
.retrieveProfileAvatar(account.getSelfRecipientAddress())
|
|
||||||
: null;
|
|
||||||
try (streamDetails) {
|
try (streamDetails) {
|
||||||
final var avatarUploadParams = streamDetails != null
|
final var avatarUploadParams = streamDetails != null
|
||||||
? AvatarUploadParams.forAvatar(streamDetails)
|
? AvatarUploadParams.forAvatar(streamDetails)
|
||||||
@ -265,9 +261,7 @@ public final class ProfileHelper {
|
|||||||
try {
|
try {
|
||||||
blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE, false));
|
blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE, false));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warn("Failed to retrieve profile for {}, ignoring: {}",
|
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
||||||
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
|
||||||
e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return account.getProfileStore().getProfile(recipientId);
|
return account.getProfileStore().getProfile(recipientId);
|
||||||
@ -385,9 +379,7 @@ 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 for {}, ignoring: {}",
|
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
||||||
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,10 +9,10 @@ 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;
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
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.websocket.SignalWebSocket;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||||
@ -148,19 +148,15 @@ 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) {
|
||||||
if (it instanceof EnvelopeResponse.Unparseable) {
|
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
|
||||||
logger.warn("Received unparseable envelope from server, ignoring.");
|
it.getServerDeliveredTimestamp());
|
||||||
} else if (it instanceof EnvelopeResponse.Parsed parsed) {
|
final var recipientId = envelope1.getSourceServiceId()
|
||||||
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(parsed.getEnvelope(),
|
.map(ServiceId::parseOrNull)
|
||||||
parsed.getServerDeliveredTimestamp());
|
.map(s -> account.getRecipientResolver().resolveRecipient(s))
|
||||||
final var sourceServiceId = envelope1.getSourceServiceId();
|
.orElse(null);
|
||||||
final var recipientId = sourceServiceId == null
|
logger.trace("Storing new message from {}", recipientId);
|
||||||
? null
|
// store message on disk, before acknowledging receipt to the server
|
||||||
: account.getRecipientResolver().resolveRecipient(sourceServiceId);
|
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
||||||
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) {
|
||||||
@ -172,9 +168,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -245,7 +238,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() == null && address.aci().isPresent()) {
|
if (envelope.getSourceServiceId().isEmpty() && 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 {
|
||||||
@ -299,7 +292,7 @@ public class ReceiveHelper {
|
|||||||
cachedMessage.delete();
|
cachedMessage.delete();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (envelope.getSourceServiceId() == null) {
|
if (envelope.getSourceServiceId().isEmpty()) {
|
||||||
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));
|
||||||
|
|||||||
@ -11,13 +11,13 @@ import org.signal.core.models.ServiceId.ACI;
|
|||||||
import org.signal.core.models.ServiceId.PNI;
|
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.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;
|
||||||
|
|||||||
@ -35,8 +35,6 @@ 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.calls.SignalServiceCallMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||||
@ -332,91 +330,6 @@ public class SendHelper {
|
|||||||
return 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,
|
||||||
@ -567,7 +480,7 @@ public class SendHelper {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
|
||||||
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);
|
||||||
@ -575,10 +488,6 @@ 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);
|
||||||
@ -598,31 +507,9 @@ public class SendHelper {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Recipients that are already known to be unregistered are skipped here.
|
final var addressesMap = recipientIds.stream()
|
||||||
// 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(targetRecipientIds);
|
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds);
|
||||||
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()))
|
||||||
@ -636,19 +523,17 @@ 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()
|
||||||
: targetRecipientIds.stream()
|
: recipientIds.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.",
|
logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
|
||||||
senderKeyTargets.size(),
|
|
||||||
targetRecipientIds.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
|
final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
|
||||||
@ -684,11 +569,11 @@ public class SendHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var legacyTargets = new HashSet<>(targetRecipientIds);
|
final var legacyTargets = new HashSet<>(recipientIds);
|
||||||
legacyTargets.removeAll(senderKeyTargets);
|
legacyTargets.removeAll(senderKeyTargets);
|
||||||
final boolean onlyTargetIsSelfWithLinkedDevice = targetRecipientIds.isEmpty() && account.isMultiDevice();
|
final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
|
||||||
|
|
||||||
if (legacySender != null && (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice)) {
|
if (!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 {
|
||||||
@ -705,11 +590,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,
|
||||||
@ -720,7 +605,6 @@ 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;
|
||||||
@ -783,7 +667,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 | NoSessionException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -975,7 +859,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, NoSessionException;
|
) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SenderKeySenderHandler {
|
interface SenderKeySenderHandler {
|
||||||
@ -995,6 +879,6 @@ public class SendHelper {
|
|||||||
List<SignalServiceAddress> recipients,
|
List<SignalServiceAddress> recipients,
|
||||||
List<SealedSenderAccess> unidentifiedAccess,
|
List<SealedSenderAccess> unidentifiedAccess,
|
||||||
boolean isRecipientUpdate
|
boolean isRecipientUpdate
|
||||||
) throws IOException, UntrustedIdentityException, NoSessionException;
|
) throws IOException, UntrustedIdentityException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,19 +2,14 @@ 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;
|
||||||
@ -22,9 +17,6 @@ import org.asamk.signal.manager.util.KeyUtils;
|
|||||||
import org.signal.core.models.storageservice.StorageKey;
|
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;
|
||||||
@ -33,6 +25,9 @@ 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.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;
|
||||||
|
|
||||||
@ -43,7 +38,6 @@ 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;
|
||||||
@ -56,19 +50,16 @@ 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 {
|
||||||
@ -87,7 +78,6 @@ 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) {
|
||||||
@ -128,7 +118,6 @@ 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;
|
||||||
|
|
||||||
@ -150,10 +139,7 @@ public class StorageHelper {
|
|||||||
needsForcePush = true;
|
needsForcePush = true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
needsMultiDeviceSync = writeToStorage(storageKey,
|
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
||||||
remoteManifest,
|
|
||||||
needsForcePush,
|
|
||||||
fetchedRemoteManifest);
|
|
||||||
} catch (RetryLaterException e) {
|
} catch (RetryLaterException e) {
|
||||||
// TODO retry later
|
// TODO retry later
|
||||||
return;
|
return;
|
||||||
@ -225,26 +211,20 @@ public class StorageHelper {
|
|||||||
remoteOnlyRecords.size());
|
remoteOnlyRecords.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
final var listListPair = processKnownRecords(connection, remoteOnlyRecords);
|
// This logic is wrong, records should only be deleted if they're deleted remotely, not if the remote record is updated
|
||||||
final var unknownInserts = listListPair.first();
|
// if (!idDifference.localOnlyIds().isEmpty()) {
|
||||||
final var updatedStorageIds = listListPair.second();
|
// final var updated = account.getRecipientStore()
|
||||||
final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds());
|
// .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
||||||
updatedStorageIds.forEach(oldUnregisteredLocalOnlyIds::remove);
|
// idDifference.localOnlyIds());
|
||||||
if (!idDifference.localOnlyIds().isEmpty()) {
|
//
|
||||||
final var updated = account.getRecipientStore()
|
// if (updated > 0) {
|
||||||
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
// logger.warn(
|
||||||
oldUnregisteredLocalOnlyIds);
|
// "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
|
||||||
final var updatedStickers = account.getStickerStore()
|
// updated);
|
||||||
.removeStorageIdsFromLocalOnlyDeletedStickerPacks(connection, oldUnregisteredLocalOnlyIds);
|
// }
|
||||||
|
// }
|
||||||
if (updated > 0 || updatedStickers > 0) {
|
//
|
||||||
logger.warn(
|
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
|
||||||
"Found {} recipients and {} sticker packs that were deleted remotely but only marked deleted locally. Removed those from local store.",
|
|
||||||
updated,
|
|
||||||
updatedStickers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()))
|
||||||
@ -295,8 +275,7 @@ 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()) {
|
||||||
@ -334,19 +313,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,18 +331,11 @@ 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 -> {
|
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
||||||
storageSyncLoopDetector.onWriteFailed();
|
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
||||||
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());
|
||||||
@ -402,7 +361,6 @@ 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);
|
||||||
@ -449,19 +407,6 @@ 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);
|
||||||
@ -512,7 +457,6 @@ 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);
|
||||||
@ -536,21 +480,13 @@ 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,
|
||||||
_ -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
|
recipientId -> 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,
|
||||||
_ -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
recipientId -> 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(
|
||||||
@ -568,7 +504,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 StorageServiceService.StorageRecordResult.DecryptionError decryptionError -> {
|
case StorageServiceRepository.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();
|
||||||
@ -578,11 +514,11 @@ public class StorageHelper {
|
|||||||
throw new IOException(decryptionError.getException());
|
throw new IOException(decryptionError.getException());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case StorageServiceService.StorageRecordResult.NetworkError networkError ->
|
case StorageServiceRepository.StorageRecordResult.NetworkError networkError ->
|
||||||
throw networkError.getException();
|
throw networkError.getException();
|
||||||
case StorageServiceService.StorageRecordResult.StatusCodeError statusCodeError ->
|
case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError ->
|
||||||
throw statusCodeError.getException();
|
throw statusCodeError.getException();
|
||||||
case StorageServiceService.StorageRecordResult.Success success -> success.getRecords();
|
case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords();
|
||||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -592,7 +528,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -641,14 +576,6 @@ 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);
|
||||||
}
|
}
|
||||||
@ -703,25 +630,16 @@ public class StorageHelper {
|
|||||||
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
|
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<List<StorageId>, List<StorageId>> processKnownRecords(
|
private 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) {
|
||||||
@ -740,21 +658,12 @@ 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 new Pair<>(unknownRecords, processedRecords);
|
return unknownRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -38,7 +38,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOper
|
|||||||
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.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;
|
||||||
@ -132,14 +131,11 @@ 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 streamDetails = new StreamDetails(groupsFileStream,
|
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
|
||||||
MimeUtils.OCTET_STREAM,
|
|
||||||
groupsFile.length());
|
|
||||||
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
|
||||||
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
||||||
.withStream(streamDetails.getStream())
|
.withStream(groupsFileStream)
|
||||||
.withContentType(streamDetails.getContentType())
|
.withContentType(MimeUtils.OCTET_STREAM)
|
||||||
.withLength(streamDetails.getLength())
|
.withLength(groupsFile.length())
|
||||||
.withResumableUploadSpec(uploadSpec)
|
.withResumableUploadSpec(uploadSpec)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -160,7 +156,7 @@ public class SyncHelper {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
try (OutputStream fos = new FileOutputStream(contactsFile)) {
|
try (OutputStream fos = new FileOutputStream(contactsFile)) {
|
||||||
var out = new DeviceContactsOutputStream(fos);
|
var out = new DeviceContactsOutputStream(fos, true, true);
|
||||||
for (var contactPair : account.getContactStore().getContacts()) {
|
for (var contactPair : account.getContactStore().getContacts()) {
|
||||||
final var recipientId = contactPair.first();
|
final var recipientId = contactPair.first();
|
||||||
final var contact = contactPair.second();
|
final var contact = contactPair.second();
|
||||||
@ -194,14 +190,11 @@ 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 streamDetails = new StreamDetails(contactsFileStream,
|
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
|
||||||
MimeUtils.OCTET_STREAM,
|
|
||||||
contactsFile.length());
|
|
||||||
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
|
||||||
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
||||||
.withStream(streamDetails.getStream())
|
.withStream(contactsFileStream)
|
||||||
.withContentType(streamDetails.getContentType())
|
.withContentType(MimeUtils.OCTET_STREAM)
|
||||||
.withLength(streamDetails.getLength())
|
.withLength(contactsFile.length())
|
||||||
.withResumableUploadSpec(uploadSpec)
|
.withResumableUploadSpec(uploadSpec)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -237,19 +230,18 @@ 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 groups = new ArrayList<BlockedListMessage.Group>();
|
var groupIds = new ArrayList<byte[]>();
|
||||||
for (var record : account.getGroupStore().getGroups()) {
|
for (var record : account.getGroupStore().getGroups()) {
|
||||||
if (record.isBlocked()) {
|
if (record.isBlocked()) {
|
||||||
groups.add(new BlockedListMessage.Group(record.getGroupId().serialize(), record.getBlockedAt()));
|
groupIds.add(record.getGroupId().serialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return context.getSendHelper()
|
return context.getSendHelper()
|
||||||
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groups)));
|
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendVerifiedMessage(
|
public SendMessageResult sendVerifiedMessage(
|
||||||
|
|||||||
@ -83,7 +83,6 @@ 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;
|
||||||
@ -98,23 +97,16 @@ import org.asamk.signal.manager.util.StickerUtils;
|
|||||||
import org.signal.core.models.ServiceId;
|
import org.signal.core.models.ServiceId;
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
|
import org.signal.core.util.Base64;
|
||||||
import org.signal.core.util.Hex;
|
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.calls.AnswerMessage;
|
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||||
@ -122,8 +114,11 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
|||||||
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
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.Util;
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
@ -160,7 +155,6 @@ 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 {
|
||||||
@ -196,7 +190,6 @@ 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());
|
||||||
@ -250,11 +243,6 @@ 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();
|
||||||
@ -290,7 +278,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(e.getRetryAfterSeconds() * 1000L);
|
throw new RateLimitException(System.currentTimeMillis() + e.getRetryAfterSeconds() * 1000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
|
return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
|
||||||
@ -300,7 +288,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
|
||||||
@ -327,7 +315,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
|
||||||
@ -484,26 +472,23 @@ public class ManagerImpl implements Manager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Device> getLinkedDevices() throws IOException {
|
public List<Device> getLinkedDevices() throws IOException {
|
||||||
final List<LinkedDevice> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||||
.getDevices(cont));
|
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
var identityKey = account.getAciIdentityKeyPair();
|
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
return devices.stream().map(d -> {
|
return devices.stream().map(d -> {
|
||||||
String deviceName = null;
|
String deviceName = d.getName();
|
||||||
if (d.getEncryptedName() != null && d.getEncryptedName().length > 0) {
|
if (deviceName != null) {
|
||||||
try {
|
try {
|
||||||
deviceName = new String(DeviceNameCipher.decryptDeviceName(DeviceName.ADAPTER.decode(d.getEncryptedName()),
|
deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
|
||||||
identityKey), StandardCharsets.UTF_8);
|
} catch (IOException e) {
|
||||||
} 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().toEpochMilli(),
|
d.getLastSeen(),
|
||||||
d.getId() == account.getDeviceId());
|
d.getId() == account.getDeviceId());
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
@ -523,7 +508,7 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getPlaintextCreatedAt(LinkedDevice d) {
|
private Long getPlaintextCreatedAt(DeviceInfo d) {
|
||||||
final var DECRYPTION_INFO = "deviceCreatedAt";
|
final var DECRYPTION_INFO = "deviceCreatedAt";
|
||||||
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
try {
|
try {
|
||||||
@ -531,9 +516,8 @@ 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(d.getCreatedAtCiphertext(),
|
var createdAtPlaintext = identityKey.open(Base64.decode(d.getCreatedAtCiphertext()
|
||||||
DECRYPTION_INFO,
|
.getBytes(StandardCharsets.UTF_8)), DECRYPTION_INFO, associatedData.toByteArray());
|
||||||
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);
|
||||||
@ -610,11 +594,6 @@ 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,
|
||||||
@ -715,10 +694,7 @@ public class ManagerImpl implements Manager {
|
|||||||
)) {
|
)) {
|
||||||
final var result = notifySelf
|
final var result = notifySelf
|
||||||
? context.getSendHelper()
|
? context.getSendHelper()
|
||||||
.sendMessage(messageBuilder,
|
.sendMessage(messageBuilder, account.getSelfRecipientId(), editTargetTimestamp, urgent)
|
||||||
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) {
|
||||||
@ -806,20 +782,13 @@ public class ManagerImpl implements Manager {
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
|
final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
|
||||||
List<SendMessageResult> results;
|
final var result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
|
||||||
if (receiptMessage.isDeliveryReceipt() || !Boolean.FALSE.equals(account.getConfigurationStore()
|
|
||||||
.getReadReceipts())) {
|
|
||||||
final var result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
|
|
||||||
results = List.of(toSendMessageResult(result));
|
|
||||||
} else {
|
|
||||||
results = List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
final var aci = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).aci();
|
final var aci = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).aci();
|
||||||
if (aci.isPresent()) {
|
if (aci.isPresent()) {
|
||||||
context.getSyncHelper().sendSyncReceiptMessage(aci.get(), receiptMessage);
|
context.getSyncHelper().sendSyncReceiptMessage(aci.get(), receiptMessage);
|
||||||
}
|
}
|
||||||
return new SendMessageResults(timestamp, Map.of(sender, results));
|
return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result))));
|
||||||
} 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()))));
|
||||||
@ -853,98 +822,6 @@ public class ManagerImpl implements Manager {
|
|||||||
return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp), message.urgent());
|
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(
|
||||||
final SignalServiceDataMessage.Builder messageBuilder,
|
final SignalServiceDataMessage.Builder messageBuilder,
|
||||||
final Message message
|
final Message message
|
||||||
@ -956,10 +833,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);
|
||||||
@ -1027,7 +904,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 = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||||
final var stickerAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
final var stickerAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
uploadSpec);
|
uploadSpec);
|
||||||
@ -1041,7 +918,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(),
|
||||||
@ -1211,26 +1088,30 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
|
public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
|
||||||
for (var recipient : recipients) {
|
var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
|
||||||
final RecipientId recipientId;
|
|
||||||
try {
|
try {
|
||||||
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
return sendMessage(messageBuilder,
|
||||||
} catch (UnregisteredRecipientException e) {
|
recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()),
|
||||||
continue;
|
false);
|
||||||
}
|
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
|
||||||
final var recipientAddress = context.getAccount()
|
throw new AssertionError(e);
|
||||||
.getRecipientAddressResolver()
|
} finally {
|
||||||
.resolveRecipientAddress(recipientId);
|
for (var recipient : recipients) {
|
||||||
final var aciSessionStore = account.getAccountData(ServiceIdType.ACI).getSessionStore();
|
final RecipientId recipientId;
|
||||||
final var pniSessionStore = account.getAccountData(ServiceIdType.PNI).getSessionStore();
|
try {
|
||||||
if (recipientAddress.aci().isPresent()) {
|
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||||
aciSessionStore.archiveSessions(recipientAddress.aci().get());
|
} catch (UnregisteredRecipientException e) {
|
||||||
pniSessionStore.archiveSessions(recipientAddress.aci().get());
|
continue;
|
||||||
}
|
}
|
||||||
if (recipientAddress.pni().isPresent()) {
|
final var serviceId = context.getAccount()
|
||||||
aciSessionStore.archiveSessions(recipientAddress.pni().get());
|
.getRecipientAddressResolver()
|
||||||
pniSessionStore.archiveSessions(recipientAddress.pni().get());
|
.resolveRecipientAddress(recipientId)
|
||||||
|
.serviceId();
|
||||||
|
if (serviceId.isPresent()) {
|
||||||
|
account.getAccountData(ServiceIdType.ACI).getSessionStore().deleteAllSessions(serviceId.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1715,15 +1596,8 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final var contact = account.getContactStore().getContact(recipientId);
|
final var contact = account.getContactStore().getContact(recipientId);
|
||||||
if (contact != null) {
|
if (contact != null && !Util.isEmpty(contact.getName())) {
|
||||||
final var nickname = contact.getDisplayNickname();
|
return contact.getName();
|
||||||
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);
|
||||||
@ -1944,10 +1818,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, null);
|
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1963,10 +1835,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, null);
|
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1982,10 +1852,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, null);
|
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2008,10 +1876,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2026,10 +1892,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, null);
|
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ 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;
|
||||||
|
|
||||||
@ -96,47 +95,23 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Manager getManager(final String identifier) {
|
public Manager getManager(final String number) {
|
||||||
synchronized (managers) {
|
synchronized (managers) {
|
||||||
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
final var manager = managers.stream()
|
||||||
// Check if UUID corresponds to an already-loaded manager
|
.filter(m -> m.getSelfNumber().equals(number))
|
||||||
final var existing = managers.stream()
|
.findFirst()
|
||||||
.filter(m -> m.getSelfACI().equals(identifier))
|
.orElse(null);
|
||||||
.findFirst()
|
if (manager != null) {
|
||||||
.orElse(null);
|
return manager;
|
||||||
if (existing != null) {
|
}
|
||||||
logger.debug("Found already loaded manager for ACI: {}", identifier);
|
try {
|
||||||
return existing;
|
final var newManager = signalAccountFiles.initManager(number);
|
||||||
}
|
managers.add(newManager);
|
||||||
// Load by ACI
|
return newManager;
|
||||||
try {
|
} catch (IOException | NotRegisteredException | AccountCheckException e) {
|
||||||
final var newManager = signalAccountFiles.initManagerByAci(identifier);
|
logger.warn("Failed to load new manager", e);
|
||||||
managers.add(newManager);
|
return null;
|
||||||
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,57 +19,34 @@ 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.internal.crypto.SecondaryProvisioningCipher;
|
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
|
||||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage;
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
|
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 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;
|
|
||||||
|
|
||||||
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
|
||||||
|
|
||||||
public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
public class ProvisioningManagerImpl implements ProvisioningManager {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
|
private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
|
||||||
|
|
||||||
@ -79,10 +56,9 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
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,
|
||||||
@ -97,58 +73,35 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
this.newManagerListener = newManagerListener;
|
this.newManagerListener = newManagerListener;
|
||||||
this.accountsStore = accountsStore;
|
this.accountsStore = accountsStore;
|
||||||
|
|
||||||
final IdentityKeyPair tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||||
password = KeyUtils.createPassword();
|
password = KeyUtils.createPassword();
|
||||||
|
final var credentialsProvider = new DynamicCredentialsProvider(null,
|
||||||
socketHandle = ProvisioningSocket.Companion.start(new ProvisioningSocket.Mode.Link(false),
|
null,
|
||||||
tempIdentityKey,
|
null,
|
||||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
password,
|
||||||
(id, t) -> {
|
SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||||
urlFuture.completeExceptionally(t);
|
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
messageFuture.completeExceptionally(t);
|
credentialsProvider,
|
||||||
},
|
userAgent,
|
||||||
new ProvisioningBlock());
|
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||||
|
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 {
|
||||||
try {
|
var deviceUuid = provisioningApi.getNewDeviceUuid();
|
||||||
var url = urlFuture.get(30, TimeUnit.SECONDS);
|
|
||||||
return new URI(url);
|
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
||||||
} 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 {
|
||||||
SecondaryProvisioningCipher.ProvisioningDecryptResult<ProvisionMessage> decryptResult;
|
var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
|
||||||
try {
|
var number = ret.getNumber();
|
||||||
decryptResult = messageFuture.get(120, TimeUnit.SECONDS);
|
var aci = ret.getAci();
|
||||||
} catch (java.util.concurrent.TimeoutException e) {
|
var pni = ret.getPni();
|
||||||
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);
|
||||||
|
|
||||||
@ -167,42 +120,21 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
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
|
||||||
: DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), aciIdentity);
|
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getAciIdentity().getPrivateKey());
|
||||||
var accountEntropyPool = msg.accountEntropyPool == null ? null : new AccountEntropyPool(msg.accountEntropyPool);
|
// Create new account with the synced identity
|
||||||
var mediaRootBackupKey = msg.mediaRootBackupKey == null
|
var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
|
||||||
? 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,
|
||||||
@ -210,43 +142,24 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
pni,
|
pni,
|
||||||
password,
|
password,
|
||||||
encryptedDeviceName,
|
encryptedDeviceName,
|
||||||
aciIdentity,
|
ret.getAciIdentity(),
|
||||||
pniIdentity,
|
ret.getPniIdentity(),
|
||||||
profileKey,
|
profileKey,
|
||||||
accountEntropyPool,
|
ret.getAccountEntropyPool(),
|
||||||
msg.authCredentialSalt == null ? null : msg.authCredentialSalt.toByteArray(),
|
ret.getMediaRootBackupKey());
|
||||||
mediaRootBackupKey);
|
|
||||||
|
|
||||||
if (msg.readReceipts != null) {
|
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
|
||||||
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");
|
||||||
final var attrs = account.getAccountAttributes(null);
|
var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
||||||
final var deviceAttributes = new DeviceAttributes(attrs.getFetchesMessages(),
|
account.getAccountAttributes(null),
|
||||||
attrs.getRegistrationId(),
|
aciPreKeys,
|
||||||
attrs.getPniRegistrationId(),
|
pniPreKeys);
|
||||||
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 {
|
||||||
@ -283,12 +196,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
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();
|
||||||
@ -296,33 +203,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
@ -336,10 +216,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@ -367,36 +243,4 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -231,7 +231,6 @@ 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()
|
||||||
|
|||||||
@ -3,21 +3,9 @@ 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.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;
|
||||||
@ -26,21 +14,29 @@ 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.calling.CallingApi;
|
||||||
|
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.keys.PreKeyRepository;
|
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
||||||
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;
|
||||||
@ -65,7 +61,6 @@ 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;
|
||||||
|
|
||||||
@ -87,11 +82,6 @@ public class SignalDependencies {
|
|||||||
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;
|
||||||
@ -101,13 +91,14 @@ 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
|
||||||
) {
|
) {
|
||||||
@ -115,7 +106,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -253,8 +243,8 @@ public class SignalDependencies {
|
|||||||
getPushServiceSocket()));
|
getPushServiceSocket()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public StorageServiceService getStorageServiceRepository() {
|
public StorageServiceRepository getStorageServiceRepository() {
|
||||||
return new StorageServiceService(getStorageServiceApi());
|
return new StorageServiceRepository(getStorageServiceApi());
|
||||||
}
|
}
|
||||||
|
|
||||||
public CertificateApi getCertificateApi() {
|
public CertificateApi getCertificateApi() {
|
||||||
@ -311,7 +301,7 @@ public class SignalDependencies {
|
|||||||
getLibSignalNetwork(),
|
getLibSignalNetwork(),
|
||||||
credentialsProvider,
|
credentialsProvider,
|
||||||
allowStories,
|
allowStories,
|
||||||
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(30));
|
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
||||||
healthMonitor.monitor(authenticatedSignalWebSocket);
|
healthMonitor.monitor(authenticatedSignalWebSocket);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -326,7 +316,7 @@ public class SignalDependencies {
|
|||||||
getLibSignalNetwork(),
|
getLibSignalNetwork(),
|
||||||
null,
|
null,
|
||||||
allowStories,
|
allowStories,
|
||||||
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(30));
|
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
||||||
healthMonitor.monitor(unauthenticatedSignalWebSocket);
|
healthMonitor.monitor(unauthenticatedSignalWebSocket);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -336,34 +326,12 @@ 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(),
|
||||||
@ -371,7 +339,8 @@ public class SignalDependencies {
|
|||||||
ServiceConfig.MAX_ENVELOPE_SIZE,
|
ServiceConfig.MAX_ENVELOPE_SIZE,
|
||||||
ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE,
|
ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE,
|
||||||
() -> true,
|
() -> true,
|
||||||
getPreKeyRepository()));
|
true,
|
||||||
|
true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SecureValueRecovery> getSecureValueRecovery() {
|
public List<SecureValueRecovery> getSecureValueRecovery() {
|
||||||
@ -399,10 +368,7 @@ 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 serviceId = serviceIdType == ServiceIdType.ACI
|
final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164());
|
||||||
? 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,
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
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;
|
||||||
@ -96,26 +94,6 @@ 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.
|
||||||
|
|||||||
@ -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 = 31;
|
private static final long DATABASE_VERSION = 28;
|
||||||
|
|
||||||
private AccountDatabase(final HikariDataSource dataSource) {
|
private AccountDatabase(final HikariDataSource dataSource) {
|
||||||
super(logger, DATABASE_VERSION, dataSource);
|
super(logger, DATABASE_VERSION, dataSource);
|
||||||
@ -623,36 +623,6 @@ 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(
|
||||||
|
|||||||
@ -44,8 +44,7 @@ public class AttachmentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public StreamDetails retrieveAttachment(final String id) throws IOException {
|
public StreamDetails retrieveAttachment(final String id) throws IOException {
|
||||||
final var safeId = sanitizeId(id);
|
final var attachmentFile = new File(attachmentsPath, id);
|
||||||
final var attachmentFile = new File(attachmentsPath, safeId);
|
|
||||||
return Utils.createStreamDetailsFromFile(attachmentFile);
|
return Utils.createStreamDetailsFromFile(attachmentFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,8 +61,7 @@ public class AttachmentStore {
|
|||||||
Optional<String> contentType
|
Optional<String> contentType
|
||||||
) {
|
) {
|
||||||
final var extension = getAttachmentExtension(filename, contentType);
|
final var extension = getAttachmentExtension(filename, contentType);
|
||||||
final var safe = sanitizeId(attachmentId.toString());
|
return new File(attachmentsPath, attachmentId.toString() + extension + ".preview");
|
||||||
return new File(attachmentsPath, safe + extension + ".preview");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getAttachmentFile(
|
private File getAttachmentFile(
|
||||||
@ -72,15 +70,7 @@ public class AttachmentStore {
|
|||||||
Optional<String> contentType
|
Optional<String> contentType
|
||||||
) {
|
) {
|
||||||
final var extension = getAttachmentExtension(filename, contentType);
|
final var extension = getAttachmentExtension(filename, contentType);
|
||||||
final var safe = sanitizeId(attachmentId.toString());
|
return new File(attachmentsPath, attachmentId.toString() + extension);
|
||||||
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) {
|
||||||
|
|||||||
@ -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 = 11;
|
private static final int CURRENT_STORAGE_VERSION = 10;
|
||||||
|
|
||||||
private final Object LOCK = new Object();
|
private final Object LOCK = new Object();
|
||||||
|
|
||||||
@ -141,7 +141,6 @@ 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;
|
||||||
|
|
||||||
@ -154,10 +153,6 @@ 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);
|
||||||
@ -197,10 +192,6 @@ 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,
|
||||||
@ -297,12 +288,11 @@ public class SignalAccount implements Closeable {
|
|||||||
final ACI aci,
|
final ACI aci,
|
||||||
final PNI pni,
|
final PNI pni,
|
||||||
final String password,
|
final String password,
|
||||||
final byte[] encryptedDeviceName,
|
final String encryptedDeviceName,
|
||||||
final IdentityKeyPair aciIdentity,
|
final IdentityKeyPair aciIdentity,
|
||||||
final IdentityKeyPair pniIdentity,
|
final IdentityKeyPair pniIdentity,
|
||||||
final ProfileKey profileKey,
|
final ProfileKey profileKey,
|
||||||
final AccountEntropyPool accountEntropyPool,
|
final AccountEntropyPool accountEntropyPool,
|
||||||
final byte[] authCredentialSalt,
|
|
||||||
final MediaRootBackupKey mediaRootBackupKey
|
final MediaRootBackupKey mediaRootBackupKey
|
||||||
) {
|
) {
|
||||||
this.deviceId = 0;
|
this.deviceId = 0;
|
||||||
@ -313,13 +303,12 @@ 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 = org.signal.core.util.Base64.encodeWithoutPadding(encryptedDeviceName);
|
this.encryptedDeviceName = 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;
|
||||||
@ -327,7 +316,6 @@ public class SignalAccount implements Closeable {
|
|||||||
this.pinMasterKey = null;
|
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);
|
||||||
@ -364,7 +352,6 @@ 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;
|
||||||
@ -377,7 +364,6 @@ 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);
|
||||||
@ -530,9 +516,6 @@ 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));
|
||||||
}
|
}
|
||||||
@ -913,7 +896,6 @@ public class SignalAccount implements Closeable {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
contact.blocked,
|
contact.blocked,
|
||||||
0,
|
|
||||||
contact.archived,
|
contact.archived,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -959,7 +941,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);
|
||||||
@ -1022,7 +1004,6 @@ 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()),
|
||||||
@ -1237,11 +1218,6 @@ 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();
|
||||||
@ -1662,10 +1638,6 @@ 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) {
|
||||||
@ -1749,7 +1721,7 @@ public class SignalAccount implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRegistered() {
|
public boolean isRegistered() {
|
||||||
return registered && deviceId > 0;
|
return registered;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegistered(final boolean registered) {
|
public void setRegistered(final boolean registered) {
|
||||||
@ -1777,14 +1749,6 @@ 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);
|
||||||
}
|
}
|
||||||
@ -1956,8 +1920,7 @@ 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() {
|
||||||
@ -2005,7 +1968,6 @@ 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,
|
||||||
|
|||||||
@ -58,10 +58,6 @@ 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);
|
||||||
@ -70,8 +66,6 @@ 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();
|
||||||
|
|||||||
@ -24,7 +24,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
public String color;
|
public String color;
|
||||||
public int messageExpirationTime;
|
public int messageExpirationTime;
|
||||||
public boolean blocked;
|
public boolean blocked;
|
||||||
private long blockedAt;
|
|
||||||
public boolean archived;
|
public boolean archived;
|
||||||
private byte[] storageRecord;
|
private byte[] storageRecord;
|
||||||
|
|
||||||
@ -40,7 +39,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
final String color,
|
final String color,
|
||||||
final int messageExpirationTime,
|
final int messageExpirationTime,
|
||||||
final boolean blocked,
|
final boolean blocked,
|
||||||
final long blockedAt,
|
|
||||||
final boolean archived,
|
final boolean archived,
|
||||||
final byte[] storageRecord
|
final byte[] storageRecord
|
||||||
) {
|
) {
|
||||||
@ -51,7 +49,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
this.color = color;
|
this.color = color;
|
||||||
this.messageExpirationTime = messageExpirationTime;
|
this.messageExpirationTime = messageExpirationTime;
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
this.blockedAt = blockedAt;
|
|
||||||
this.archived = archived;
|
this.archived = archived;
|
||||||
this.storageRecord = storageRecord;
|
this.storageRecord = storageRecord;
|
||||||
}
|
}
|
||||||
@ -94,24 +91,9 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBlocked(final boolean blocked) {
|
public void setBlocked(final boolean blocked) {
|
||||||
if (blocked && !this.blocked) {
|
|
||||||
blockedAt = System.currentTimeMillis();
|
|
||||||
} else if (!blocked) {
|
|
||||||
blockedAt = 0;
|
|
||||||
}
|
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBlockedAt() {
|
|
||||||
return blockedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setBlockedAt(final long blockedAt) {
|
|
||||||
this.blockedAt = blockedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isProfileSharingEnabled() {
|
public boolean isProfileSharingEnabled() {
|
||||||
return true;
|
return true;
|
||||||
@ -131,11 +113,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isTerminated() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupPermission getPermissionAddMember() {
|
public GroupPermission getPermissionAddMember() {
|
||||||
return GroupPermission.EVERY_MEMBER;
|
return GroupPermission.EVERY_MEMBER;
|
||||||
|
|||||||
@ -23,7 +23,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
private final GroupMasterKey masterKey;
|
private final GroupMasterKey masterKey;
|
||||||
private final DistributionId distributionId;
|
private final DistributionId distributionId;
|
||||||
private boolean blocked;
|
private boolean blocked;
|
||||||
private long blockedAt;
|
|
||||||
private boolean profileSharingEnabled;
|
private boolean profileSharingEnabled;
|
||||||
private DecryptedGroup group;
|
private DecryptedGroup group;
|
||||||
private byte[] storageRecord;
|
private byte[] storageRecord;
|
||||||
@ -48,7 +47,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
final DecryptedGroup group,
|
final DecryptedGroup group,
|
||||||
final DistributionId distributionId,
|
final DistributionId distributionId,
|
||||||
final boolean blocked,
|
final boolean blocked,
|
||||||
final long blockedAt,
|
|
||||||
final boolean profileSharingEnabled,
|
final boolean profileSharingEnabled,
|
||||||
final boolean permissionDenied,
|
final boolean permissionDenied,
|
||||||
final byte[] storageRecord,
|
final byte[] storageRecord,
|
||||||
@ -59,7 +57,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
this.group = group;
|
this.group = group;
|
||||||
this.distributionId = distributionId;
|
this.distributionId = distributionId;
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
this.blockedAt = blockedAt;
|
|
||||||
this.profileSharingEnabled = profileSharingEnabled;
|
this.profileSharingEnabled = profileSharingEnabled;
|
||||||
this.permissionDenied = permissionDenied;
|
this.permissionDenied = permissionDenied;
|
||||||
this.storageRecord = storageRecord;
|
this.storageRecord = storageRecord;
|
||||||
@ -189,24 +186,9 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBlocked(final boolean blocked) {
|
public void setBlocked(final boolean blocked) {
|
||||||
if (blocked && !this.blocked) {
|
|
||||||
blockedAt = System.currentTimeMillis();
|
|
||||||
} else if (!blocked) {
|
|
||||||
blockedAt = 0;
|
|
||||||
}
|
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBlockedAt() {
|
|
||||||
return blockedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setBlockedAt(final long blockedAt) {
|
|
||||||
this.blockedAt = blockedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isProfileSharingEnabled() {
|
public boolean isProfileSharingEnabled() {
|
||||||
return profileSharingEnabled;
|
return profileSharingEnabled;
|
||||||
@ -229,11 +211,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
return this.group != null && this.group.isAnnouncementGroup == EnabledState.ENABLED;
|
return this.group != null && this.group.isAnnouncementGroup == EnabledState.ENABLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isTerminated() {
|
|
||||||
return this.group != null && Boolean.TRUE.equals(this.group.terminated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupPermission getPermissionAddMember() {
|
public GroupPermission getPermissionAddMember() {
|
||||||
final var accessControl = getAccessControl();
|
final var accessControl = getAccessControl();
|
||||||
|
|||||||
@ -63,7 +63,6 @@ public class GroupStore {
|
|||||||
distribution_id BLOB UNIQUE NOT NULL,
|
distribution_id BLOB UNIQUE NOT NULL,
|
||||||
endorsement_expiration_time INTEGER NOT NULL DEFAULT 0,
|
endorsement_expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
|
||||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||||
permission_denied INTEGER NOT NULL DEFAULT FALSE
|
permission_denied INTEGER NOT NULL DEFAULT FALSE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
@ -84,7 +83,6 @@ public class GroupStore {
|
|||||||
color TEXT,
|
color TEXT,
|
||||||
expiration_time INTEGER NOT NULL DEFAULT 0,
|
expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
|
||||||
archived INTEGER NOT NULL DEFAULT FALSE
|
archived INTEGER NOT NULL DEFAULT FALSE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
CREATE TABLE group_v1_member (
|
CREATE TABLE group_v1_member (
|
||||||
@ -154,7 +152,6 @@ public class GroupStore {
|
|||||||
statement.setBytes(2, groupId.serialize());
|
statement.setBytes(2, groupId.serialize());
|
||||||
final var result = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
final var result = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
connection.commit();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
internalId = result.get();
|
internalId = result.get();
|
||||||
@ -403,7 +400,6 @@ public class GroupStore {
|
|||||||
deleteGroup(connection, groupInfoV1.getGroupId());
|
deleteGroup(connection, groupInfoV1.getGroupId());
|
||||||
final var groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, recipientResolver);
|
final var groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, recipientResolver);
|
||||||
groupInfoV2.setBlocked(groupInfoV1.isBlocked());
|
groupInfoV2.setBlocked(groupInfoV1.isBlocked());
|
||||||
groupInfoV2.setBlockedAt(groupInfoV1.getBlockedAt());
|
|
||||||
updateGroup(connection, groupInfoV2);
|
updateGroup(connection, groupInfoV2);
|
||||||
logger.debug("Locally migrated group {} to group v2, id: {}",
|
logger.debug("Locally migrated group {} to group v2, id: {}",
|
||||||
groupInfoV1.getGroupId().toBase64(),
|
groupInfoV1.getGroupId().toBase64(),
|
||||||
@ -617,9 +613,9 @@ public class GroupStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
final var sql = """
|
final var sql = """
|
||||||
INSERT INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, blocked_at, archived, storage_id)
|
INSERT INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived, storage_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, group_id_v2=excluded.group_id_v2, name=excluded.name, color=excluded.color, expiration_time=excluded.expiration_time, blocked=excluded.blocked, blocked_at=excluded.blocked_at, archived=excluded.archived, storage_id=excluded.storage_id
|
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, group_id_v2=excluded.group_id_v2, name=excluded.name, color=excluded.color, expiration_time=excluded.expiration_time, blocked=excluded.blocked, archived=excluded.archived, storage_id=excluded.storage_id
|
||||||
RETURNING _id
|
RETURNING _id
|
||||||
""".formatted(TABLE_GROUP_V1);
|
""".formatted(TABLE_GROUP_V1);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
@ -634,9 +630,8 @@ public class GroupStore {
|
|||||||
statement.setString(5, groupV1.color);
|
statement.setString(5, groupV1.color);
|
||||||
statement.setLong(6, groupV1.getMessageExpirationTimer());
|
statement.setLong(6, groupV1.getMessageExpirationTimer());
|
||||||
statement.setBoolean(7, groupV1.isBlocked());
|
statement.setBoolean(7, groupV1.isBlocked());
|
||||||
statement.setLong(8, groupV1.getBlockedAt());
|
statement.setBoolean(8, groupV1.archived);
|
||||||
statement.setBoolean(9, groupV1.archived);
|
statement.setBytes(9, KeyUtils.createRawStorageId());
|
||||||
statement.setBytes(10, KeyUtils.createRawStorageId());
|
|
||||||
final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
||||||
|
|
||||||
if (internalId == null) {
|
if (internalId == null) {
|
||||||
@ -662,9 +657,9 @@ public class GroupStore {
|
|||||||
} else if (group instanceof GroupInfoV2 groupV2) {
|
} else if (group instanceof GroupInfoV2 groupV2) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, blocked_at, permission_denied, storage_id, profile_sharing)
|
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, permission_denied, storage_id, profile_sharing)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, master_key=excluded.master_key, group_data=excluded.group_data, distribution_id=excluded.distribution_id, blocked=excluded.blocked, blocked_at=excluded.blocked_at, permission_denied=excluded.permission_denied, storage_id=excluded.storage_id, profile_sharing=excluded.profile_sharing
|
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, master_key=excluded.master_key, group_data=excluded.group_data, distribution_id=excluded.distribution_id, blocked=excluded.blocked, permission_denied=excluded.permission_denied, storage_id=excluded.storage_id, profile_sharing=excluded.profile_sharing
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V2);
|
).formatted(TABLE_GROUP_V2);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
@ -682,10 +677,9 @@ public class GroupStore {
|
|||||||
}
|
}
|
||||||
statement.setBytes(5, UuidUtil.toByteArray(groupV2.getDistributionId().asUuid()));
|
statement.setBytes(5, UuidUtil.toByteArray(groupV2.getDistributionId().asUuid()));
|
||||||
statement.setBoolean(6, groupV2.isBlocked());
|
statement.setBoolean(6, groupV2.isBlocked());
|
||||||
statement.setLong(7, groupV2.getBlockedAt());
|
statement.setBoolean(7, groupV2.isPermissionDenied());
|
||||||
statement.setBoolean(8, groupV2.isPermissionDenied());
|
statement.setBytes(8, KeyUtils.createRawStorageId());
|
||||||
statement.setBytes(9, KeyUtils.createRawStorageId());
|
statement.setBoolean(9, groupV2.isProfileSharingEnabled());
|
||||||
statement.setBoolean(10, groupV2.isProfileSharingEnabled());
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -696,7 +690,7 @@ public class GroupStore {
|
|||||||
private List<GroupInfoV2> getGroupsV2() {
|
private List<GroupInfoV2> getGroupsV2() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V2);
|
).formatted(TABLE_GROUP_V2);
|
||||||
@ -714,7 +708,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
public GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id = ?
|
WHERE g.group_id = ?
|
||||||
"""
|
"""
|
||||||
@ -748,7 +742,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV2 getGroupV2(Connection connection, StorageId storageId) throws SQLException {
|
public GroupInfoV2 getGroupV2(Connection connection, StorageId storageId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.storage_id = ?
|
WHERE g.storage_id = ?
|
||||||
"""
|
"""
|
||||||
@ -771,7 +765,6 @@ public class GroupStore {
|
|||||||
final var groupData = resultSet.getBytes("group_data");
|
final var groupData = resultSet.getBytes("group_data");
|
||||||
final var distributionId = resultSet.getBytes("distribution_id");
|
final var distributionId = resultSet.getBytes("distribution_id");
|
||||||
final var blocked = resultSet.getBoolean("blocked");
|
final var blocked = resultSet.getBoolean("blocked");
|
||||||
final var blockedAt = resultSet.getLong("blocked_at");
|
|
||||||
final var profileSharingEnabled = resultSet.getBoolean("profile_sharing");
|
final var profileSharingEnabled = resultSet.getBoolean("profile_sharing");
|
||||||
final var permissionDenied = resultSet.getBoolean("permission_denied");
|
final var permissionDenied = resultSet.getBoolean("permission_denied");
|
||||||
final var storageRecord = resultSet.getBytes("storage_record");
|
final var storageRecord = resultSet.getBytes("storage_record");
|
||||||
@ -780,7 +773,6 @@ public class GroupStore {
|
|||||||
groupData == null ? null : DecryptedGroup.ADAPTER.decode(groupData),
|
groupData == null ? null : DecryptedGroup.ADAPTER.decode(groupData),
|
||||||
DistributionId.from(UuidUtil.parseOrThrow(distributionId)),
|
DistributionId.from(UuidUtil.parseOrThrow(distributionId)),
|
||||||
blocked,
|
blocked,
|
||||||
blockedAt,
|
|
||||||
profileSharingEnabled,
|
profileSharingEnabled,
|
||||||
permissionDenied,
|
permissionDenied,
|
||||||
storageRecord,
|
storageRecord,
|
||||||
@ -807,7 +799,7 @@ public class GroupStore {
|
|||||||
private List<GroupInfoV1> getGroupsV1() {
|
private List<GroupInfoV1> getGroupsV1() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1);
|
).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1);
|
||||||
@ -825,7 +817,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException {
|
public GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id = ?
|
WHERE g.group_id = ?
|
||||||
"""
|
"""
|
||||||
@ -859,7 +851,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV1 getGroupV1(Connection connection, StorageId storageId) throws SQLException {
|
public GroupInfoV1 getGroupV1(Connection connection, StorageId storageId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.storage_id = ?
|
WHERE g.storage_id = ?
|
||||||
"""
|
"""
|
||||||
@ -884,12 +876,11 @@ public class GroupStore {
|
|||||||
final var members = membersString == null
|
final var members = membersString == null
|
||||||
? Set.<RecipientId>of()
|
? Set.<RecipientId>of()
|
||||||
: Arrays.stream(membersString.split(","))
|
: Arrays.stream(membersString.split(","))
|
||||||
.map(Integer::valueOf)
|
.map(Integer::valueOf)
|
||||||
.map(recipientIdCreator::create)
|
.map(recipientIdCreator::create)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
final var expirationTime = resultSet.getInt("expiration_time");
|
final var expirationTime = resultSet.getInt("expiration_time");
|
||||||
final var blocked = resultSet.getBoolean("blocked");
|
final var blocked = resultSet.getBoolean("blocked");
|
||||||
final var blockedAt = resultSet.getLong("blocked_at");
|
|
||||||
final var archived = resultSet.getBoolean("archived");
|
final var archived = resultSet.getBoolean("archived");
|
||||||
final var storageRecord = resultSet.getBytes("storage_record");
|
final var storageRecord = resultSet.getBytes("storage_record");
|
||||||
return new GroupInfoV1(GroupId.v1(groupId),
|
return new GroupInfoV1(GroupId.v1(groupId),
|
||||||
@ -899,7 +890,6 @@ public class GroupStore {
|
|||||||
color,
|
color,
|
||||||
expirationTime,
|
expirationTime,
|
||||||
blocked,
|
blocked,
|
||||||
blockedAt,
|
|
||||||
archived,
|
archived,
|
||||||
storageRecord);
|
storageRecord);
|
||||||
}
|
}
|
||||||
@ -911,7 +901,7 @@ public class GroupStore {
|
|||||||
private GroupInfoV1 getGroupV1ByV2Id(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
private GroupInfoV1 getGroupV1ByV2Id(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id_v2 = ?
|
WHERE g.group_id_v2 = ?
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -59,7 +59,6 @@ public class LegacyGroupStore {
|
|||||||
g1.color,
|
g1.color,
|
||||||
g1.messageExpirationTime,
|
g1.messageExpirationTime,
|
||||||
g1.blocked,
|
g1.blocked,
|
||||||
0,
|
|
||||||
g1.archived,
|
g1.archived,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
@ -78,7 +77,6 @@ public class LegacyGroupStore {
|
|||||||
loadDecryptedGroupLocked(groupId, groupCachePath),
|
loadDecryptedGroupLocked(groupId, groupCachePath),
|
||||||
g2.distributionId == null ? DistributionId.create() : DistributionId.from(g2.distributionId),
|
g2.distributionId == null ? DistributionId.create() : DistributionId.from(g2.distributionId),
|
||||||
g2.blocked,
|
g2.blocked,
|
||||||
0,
|
|
||||||
true,
|
true,
|
||||||
g2.permissionDenied,
|
g2.permissionDenied,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@ -115,7 +115,7 @@ public class LegacyJsonIdentityKeyStore {
|
|||||||
var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
|
var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
|
||||||
"trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
|
"trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
|
||||||
var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
|
var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
|
||||||
.asLong()) : new Date();
|
.asLong()) : new Date();
|
||||||
identities.add(new LegacyIdentityInfo(address, id, trustLevel, added));
|
identities.add(new LegacyIdentityInfo(address, id, trustLevel, added));
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
|
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
|
||||||
|
|||||||
@ -26,7 +26,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
||||||
@ -38,7 +37,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||||||
private final IdentityKeyStore identityKeyStore;
|
private final IdentityKeyStore identityKeyStore;
|
||||||
private final SignalServiceSenderKeyStore senderKeyStore;
|
private final SignalServiceSenderKeyStore senderKeyStore;
|
||||||
private final Supplier<Boolean> isMultiDevice;
|
private final Supplier<Boolean> isMultiDevice;
|
||||||
private final Consumer<Boolean> setMultiDeviceCallback;
|
|
||||||
|
|
||||||
public SignalProtocolStore(
|
public SignalProtocolStore(
|
||||||
final SignalServicePreKeyStore preKeyStore,
|
final SignalServicePreKeyStore preKeyStore,
|
||||||
@ -47,8 +45,7 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||||||
final SignalServiceSessionStore sessionStore,
|
final SignalServiceSessionStore sessionStore,
|
||||||
final IdentityKeyStore identityKeyStore,
|
final IdentityKeyStore identityKeyStore,
|
||||||
final SignalServiceSenderKeyStore senderKeyStore,
|
final SignalServiceSenderKeyStore senderKeyStore,
|
||||||
final Supplier<Boolean> isMultiDevice,
|
final Supplier<Boolean> isMultiDevice
|
||||||
final Consumer<Boolean> setMultiDeviceCallback
|
|
||||||
) {
|
) {
|
||||||
this.preKeyStore = preKeyStore;
|
this.preKeyStore = preKeyStore;
|
||||||
this.signedPreKeyStore = signedPreKeyStore;
|
this.signedPreKeyStore = signedPreKeyStore;
|
||||||
@ -57,7 +54,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||||||
this.identityKeyStore = identityKeyStore;
|
this.identityKeyStore = identityKeyStore;
|
||||||
this.senderKeyStore = senderKeyStore;
|
this.senderKeyStore = senderKeyStore;
|
||||||
this.isMultiDevice = isMultiDevice;
|
this.isMultiDevice = isMultiDevice;
|
||||||
this.setMultiDeviceCallback = setMultiDeviceCallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -213,11 +209,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||||||
return isMultiDevice.get();
|
return isMultiDevice.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setMultiDevice(final boolean isMultiDevice) {
|
|
||||||
setMultiDeviceCallback.accept(isMultiDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KyberPreKeyRecord loadKyberPreKey(final int kyberPreKeyId) throws InvalidKeyIdException {
|
public KyberPreKeyRecord loadKyberPreKey(final int kyberPreKeyId) throws InvalidKeyIdException {
|
||||||
return kyberPreKeyStore.loadKyberPreKey(kyberPreKeyId);
|
return kyberPreKeyStore.loadKyberPreKey(kyberPreKeyId);
|
||||||
|
|||||||
@ -50,7 +50,6 @@ public class LegacyRecipientStore2 {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
r.contact.blocked,
|
r.contact.blocked,
|
||||||
0,
|
|
||||||
r.contact.archived,
|
r.contact.archived,
|
||||||
r.contact.profileSharingEnabled,
|
r.contact.profileSharingEnabled,
|
||||||
false,
|
false,
|
||||||
@ -101,7 +100,6 @@ public class LegacyRecipientStore2 {
|
|||||||
profile,
|
profile,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
false,
|
|
||||||
null);
|
null);
|
||||||
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
|
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,6 @@ public class Recipient {
|
|||||||
|
|
||||||
private final Long unregisteredTimestamp;
|
private final Long unregisteredTimestamp;
|
||||||
|
|
||||||
private final boolean pniSignatureVerified;
|
|
||||||
|
|
||||||
private final byte[] storageRecord;
|
private final byte[] storageRecord;
|
||||||
|
|
||||||
public Recipient(
|
public Recipient(
|
||||||
@ -38,7 +36,6 @@ public class Recipient {
|
|||||||
final Profile profile,
|
final Profile profile,
|
||||||
final Boolean discoverable,
|
final Boolean discoverable,
|
||||||
final Long unregisteredTimestamp,
|
final Long unregisteredTimestamp,
|
||||||
final boolean pniSignatureVerified,
|
|
||||||
final byte[] storageRecord
|
final byte[] storageRecord
|
||||||
) {
|
) {
|
||||||
this.recipientId = recipientId;
|
this.recipientId = recipientId;
|
||||||
@ -49,7 +46,6 @@ public class Recipient {
|
|||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.discoverable = discoverable;
|
this.discoverable = discoverable;
|
||||||
this.unregisteredTimestamp = unregisteredTimestamp;
|
this.unregisteredTimestamp = unregisteredTimestamp;
|
||||||
this.pniSignatureVerified = pniSignatureVerified;
|
|
||||||
this.storageRecord = storageRecord;
|
this.storageRecord = storageRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +58,6 @@ public class Recipient {
|
|||||||
profile = builder.profile;
|
profile = builder.profile;
|
||||||
discoverable = builder.discoverable;
|
discoverable = builder.discoverable;
|
||||||
unregisteredTimestamp = builder.unregisteredTimestamp;
|
unregisteredTimestamp = builder.unregisteredTimestamp;
|
||||||
pniSignatureVerified = builder.pniSignatureVerified;
|
|
||||||
storageRecord = builder.storageRecord;
|
storageRecord = builder.storageRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,9 +73,6 @@ public class Recipient {
|
|||||||
builder.profileKey = copy.getProfileKey();
|
builder.profileKey = copy.getProfileKey();
|
||||||
builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
|
builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
|
||||||
builder.profile = copy.getProfile();
|
builder.profile = copy.getProfile();
|
||||||
builder.discoverable = copy.getDiscoverable();
|
|
||||||
builder.unregisteredTimestamp = copy.getUnregisteredTimestamp();
|
|
||||||
builder.pniSignatureVerified = copy.isPniSignatureVerified();
|
|
||||||
builder.storageRecord = copy.getStorageRecord();
|
builder.storageRecord = copy.getStorageRecord();
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
@ -121,10 +113,6 @@ public class Recipient {
|
|||||||
return unregisteredTimestamp == null;
|
return unregisteredTimestamp == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPniSignatureVerified() {
|
|
||||||
return pniSignatureVerified;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getStorageRecord() {
|
public byte[] getStorageRecord() {
|
||||||
return storageRecord;
|
return storageRecord;
|
||||||
}
|
}
|
||||||
@ -139,19 +127,12 @@ public class Recipient {
|
|||||||
&& Objects.equals(contact, recipient.contact)
|
&& Objects.equals(contact, recipient.contact)
|
||||||
&& Objects.equals(profileKey, recipient.profileKey)
|
&& Objects.equals(profileKey, recipient.profileKey)
|
||||||
&& Objects.equals(expiringProfileKeyCredential, recipient.expiringProfileKeyCredential)
|
&& Objects.equals(expiringProfileKeyCredential, recipient.expiringProfileKeyCredential)
|
||||||
&& Objects.equals(profile, recipient.profile)
|
&& Objects.equals(profile, recipient.profile);
|
||||||
&& pniSignatureVerified == recipient.pniSignatureVerified;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(recipientId,
|
return Objects.hash(recipientId, address, contact, profileKey, expiringProfileKeyCredential, profile);
|
||||||
address,
|
|
||||||
contact,
|
|
||||||
profileKey,
|
|
||||||
expiringProfileKeyCredential,
|
|
||||||
profile,
|
|
||||||
pniSignatureVerified);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
@ -164,7 +145,6 @@ public class Recipient {
|
|||||||
private Profile profile;
|
private Profile profile;
|
||||||
private Boolean discoverable;
|
private Boolean discoverable;
|
||||||
private Long unregisteredTimestamp;
|
private Long unregisteredTimestamp;
|
||||||
private boolean pniSignatureVerified;
|
|
||||||
private byte[] storageRecord;
|
private byte[] storageRecord;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
@ -210,11 +190,6 @@ public class Recipient {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withPniSignatureVerified(final boolean val) {
|
|
||||||
pniSignatureVerified = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withStorageRecord(final byte[] val) {
|
public Builder withStorageRecord(final byte[] val) {
|
||||||
storageRecord = val;
|
storageRecord = val;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@ -28,9 +28,7 @@ import java.sql.Types;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -52,18 +50,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
|
|
||||||
private final Map<Long, Long> recipientsMerged = new HashMap<>();
|
private final Map<Long, Long> recipientsMerged = new HashMap<>();
|
||||||
|
|
||||||
private static final int MAX_RECIPIENT_CACHE_SIZE = 2000;
|
private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = new HashMap<>();
|
||||||
|
|
||||||
private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = Collections.synchronizedMap(new LinkedHashMap<>(
|
|
||||||
16,
|
|
||||||
0.75f,
|
|
||||||
true) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean removeEldestEntry(Map.Entry<ServiceId, RecipientWithAddress> eldest) {
|
|
||||||
return size() > MAX_RECIPIENT_CACHE_SIZE;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
public static void createSql(Connection connection) throws SQLException {
|
public static void createSql(Connection connection) throws SQLException {
|
||||||
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
|
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
|
||||||
@ -82,7 +69,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
profile_key BLOB,
|
profile_key BLOB,
|
||||||
profile_key_credential BLOB,
|
profile_key_credential BLOB,
|
||||||
needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
|
needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
|
||||||
pni_signature_verified INTEGER NOT NULL DEFAULT FALSE,
|
|
||||||
|
|
||||||
given_name TEXT,
|
given_name TEXT,
|
||||||
family_name TEXT,
|
family_name TEXT,
|
||||||
@ -96,7 +82,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
expiration_time_version INTEGER DEFAULT 1 NOT NULL,
|
expiration_time_version INTEGER DEFAULT 1 NOT NULL,
|
||||||
mute_until INTEGER NOT NULL DEFAULT 0,
|
mute_until INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
|
||||||
archived INTEGER NOT NULL DEFAULT FALSE,
|
archived INTEGER NOT NULL DEFAULT FALSE,
|
||||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||||
hide_story INTEGER NOT NULL DEFAULT FALSE,
|
hide_story INTEGER NOT NULL DEFAULT FALSE,
|
||||||
@ -199,12 +184,12 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecipientId resolveRecipient(final ServiceId serviceId) {
|
public RecipientId resolveRecipient(final ServiceId serviceId) {
|
||||||
final var recipientWithAddress = recipientAddressCache.get(serviceId);
|
|
||||||
if (recipientWithAddress != null) {
|
|
||||||
return recipientWithAddress.id();
|
|
||||||
}
|
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
connection.setAutoCommit(false);
|
connection.setAutoCommit(false);
|
||||||
|
final var recipientWithAddress = recipientAddressCache.get(serviceId);
|
||||||
|
if (recipientWithAddress != null) {
|
||||||
|
return recipientWithAddress.id();
|
||||||
|
}
|
||||||
final var recipientId = resolveRecipientLocked(connection, serviceId);
|
final var recipientId = resolveRecipientLocked(connection, serviceId);
|
||||||
connection.commit();
|
connection.commit();
|
||||||
return recipientId;
|
return recipientId;
|
||||||
@ -352,7 +337,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
public List<Pair<RecipientId, Contact>> getContacts() {
|
public List<Pair<RecipientId, Contact>> getContacts() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp
|
SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
|
||||||
FROM %s r
|
FROM %s r
|
||||||
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
|
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
|
||||||
"""
|
"""
|
||||||
@ -376,8 +361,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
SELECT r._id,
|
SELECT r._id,
|
||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -397,8 +381,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
SELECT r._id,
|
SELECT r._id,
|
||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -447,8 +430,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
SELECT r._id,
|
SELECT r._id,
|
||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -480,40 +462,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the subset of the given recipients that are currently known to be
|
|
||||||
* unregistered (i.e. have an unregistered timestamp set).
|
|
||||||
* <p>
|
|
||||||
* These can be skipped when sending group messages; otherwise every send
|
|
||||||
* re-attempts them via the slow legacy 1:1 fan-out. The unregistered flag is
|
|
||||||
* maintained independently by profile/CDS discovery and cleared again once a
|
|
||||||
* recipient registers, so they are re-included automatically.
|
|
||||||
*/
|
|
||||||
public Set<RecipientId> getUnregisteredRecipientIds(final Set<RecipientId> recipientIds) {
|
|
||||||
if (recipientIds.isEmpty()) {
|
|
||||||
return Set.of();
|
|
||||||
}
|
|
||||||
final var recipientIdsCommaSeparated = recipientIds.stream()
|
|
||||||
.map(recipientId -> String.valueOf(recipientId.id()))
|
|
||||||
.collect(Collectors.joining(","));
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT r._id
|
|
||||||
FROM %s r
|
|
||||||
WHERE r.unregistered_timestamp IS NOT NULL AND r._id IN (%s)
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_RECIPIENT, recipientIdsCommaSeparated);
|
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
try (var result = Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet)) {
|
|
||||||
return result.collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed read from recipient store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getAllNumbers() {
|
public Set<String> getAllNumbers() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
@ -893,7 +841,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, expiration_time_version = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, blocked_at = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?, hidden = ?
|
SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, expiration_time_version = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?, hidden = ?
|
||||||
WHERE _id = ?
|
WHERE _id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
@ -908,52 +856,28 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
statement.setBoolean(8, contact != null && contact.isProfileSharingEnabled());
|
statement.setBoolean(8, contact != null && contact.isProfileSharingEnabled());
|
||||||
statement.setString(9, contact == null ? null : contact.color());
|
statement.setString(9, contact == null ? null : contact.color());
|
||||||
statement.setBoolean(10, contact != null && contact.isBlocked());
|
statement.setBoolean(10, contact != null && contact.isBlocked());
|
||||||
statement.setLong(11, contact == null ? 0 : contact.blockedAt());
|
statement.setBoolean(11, contact != null && contact.isArchived());
|
||||||
statement.setBoolean(12, contact != null && contact.isArchived());
|
|
||||||
if (contact == null || contact.unregisteredTimestamp() == null) {
|
if (contact == null || contact.unregisteredTimestamp() == null) {
|
||||||
statement.setNull(13, Types.INTEGER);
|
statement.setNull(12, Types.INTEGER);
|
||||||
} else {
|
} else {
|
||||||
statement.setLong(13, contact.unregisteredTimestamp());
|
statement.setLong(12, contact.unregisteredTimestamp());
|
||||||
}
|
}
|
||||||
statement.setString(14, contact == null ? null : contact.nickNameGivenName());
|
statement.setString(13, contact == null ? null : contact.nickNameGivenName());
|
||||||
statement.setString(15, contact == null ? null : contact.nickNameFamilyName());
|
statement.setString(14, contact == null ? null : contact.nickNameFamilyName());
|
||||||
statement.setString(16, contact == null ? null : contact.note());
|
statement.setString(15, contact == null ? null : contact.note());
|
||||||
statement.setBoolean(17, contact != null && contact.isHidden());
|
statement.setBoolean(16, contact != null && contact.isHidden());
|
||||||
statement.setLong(18, recipientId.id());
|
statement.setLong(17, recipientId.id());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
if (contact != null && contact.unregisteredTimestamp() != null) {
|
if (contact != null && contact.unregisteredTimestamp() != null) {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId, contact.unregisteredTimestamp());
|
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
||||||
}
|
}
|
||||||
rotateStorageId(connection, recipientId);
|
rotateStorageId(connection, recipientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void splitForStorageSyncIfNecessary(final Connection connection, final ACI aci) throws SQLException {
|
|
||||||
final var recipient = findByServiceId(connection, aci);
|
|
||||||
if (recipient.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var recipientId = recipient.get().id();
|
|
||||||
final var address = recipient.get().address();
|
|
||||||
if (address.pni().isEmpty() && address.number().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Splitting {} for storage sync", recipientId);
|
|
||||||
final var splitAddress = new RecipientAddress(Optional.empty(),
|
|
||||||
address.pni(),
|
|
||||||
address.number(),
|
|
||||||
Optional.empty());
|
|
||||||
updateRecipientAddress(connection,
|
|
||||||
recipientId,
|
|
||||||
new RecipientAddress(address.aci(), Optional.empty(), Optional.empty(), address.username()));
|
|
||||||
resolveRecipientTrusted(connection, splitAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
|
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final Collection<StorageId> storageIds
|
final List<StorageId> storageIds
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
@ -992,69 +916,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markPniSignatureVerified(final RecipientId recipientId) {
|
|
||||||
logger.debug("Marking {} as pni signature verified", recipientId);
|
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
connection.setAutoCommit(false);
|
|
||||||
if (storePniSignatureVerified(connection, recipientId, true)) {
|
|
||||||
rotateStorageId(connection, recipientId);
|
|
||||||
}
|
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed update recipient store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean storePniSignatureVerified(
|
|
||||||
final Connection connection,
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final boolean value
|
|
||||||
) throws SQLException {
|
|
||||||
return storePniSignatureVerified(connection, recipientId, value, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean storePniSignatureVerified(
|
|
||||||
final Connection connection,
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final boolean value,
|
|
||||||
final boolean force
|
|
||||||
) throws SQLException {
|
|
||||||
if (!force && isPniSignatureVerified(connection, recipientId) == value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET pni_signature_verified = ?
|
|
||||||
WHERE _id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_RECIPIENT);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setBoolean(1, value);
|
|
||||||
statement.setLong(2, recipientId.id());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPniSignatureVerified(
|
|
||||||
final Connection connection,
|
|
||||||
final RecipientId recipientId
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT pni_signature_verified
|
|
||||||
FROM %s
|
|
||||||
WHERE _id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_RECIPIENT);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setLong(1, recipientId.id());
|
|
||||||
return Utils.executeQuerySingleRow(statement, resultSet -> resultSet.getBoolean("pni_signature_verified"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean needsPniSignature(final RecipientId recipientId) {
|
public boolean needsPniSignature(final RecipientId recipientId) {
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
@ -1086,7 +947,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
if (recipientAddress.get().address().aci().isEmpty() || (
|
if (recipientAddress.get().address().aci().isEmpty() || (
|
||||||
contact != null && contact.unregisteredTimestamp() != null
|
contact != null && contact.unregisteredTimestamp() != null
|
||||||
)) {
|
)) {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId, System.currentTimeMillis());
|
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1120,7 +981,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
if (registered) {
|
if (registered) {
|
||||||
markRegistered(connection, recipientId);
|
markRegistered(connection, recipientId);
|
||||||
} else {
|
} else {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId, System.currentTimeMillis());
|
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
||||||
}
|
}
|
||||||
connection.commit();
|
connection.commit();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@ -1130,10 +991,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
|
|
||||||
private void markUnregisteredAndSplitIfNecessary(
|
private void markUnregisteredAndSplitIfNecessary(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final RecipientId recipientId,
|
final RecipientId recipientId
|
||||||
final long unregisteredTimestamp
|
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
markUnregistered(connection, recipientId, unregisteredTimestamp);
|
markUnregistered(connection, recipientId);
|
||||||
final var address = resolveRecipientAddress(connection, recipientId);
|
final var address = resolveRecipientAddress(connection, recipientId);
|
||||||
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
||||||
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
||||||
@ -1180,11 +1040,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markUnregistered(
|
private void markUnregistered(final Connection connection, final RecipientId recipientId) throws SQLException {
|
||||||
final Connection connection,
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final long unregisteredTimestamp
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
@ -1193,7 +1049,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setLong(1, unregisteredTimestamp);
|
statement.setLong(1, System.currentTimeMillis());
|
||||||
statement.setLong(2, recipientId.id());
|
statement.setLong(2, recipientId.id());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
@ -1354,9 +1210,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final List<RecipientId> toBeMergedRecipientIds
|
final List<RecipientId> toBeMergedRecipientIds
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
for (final var toBeMergedRecipientId : toBeMergedRecipientIds) {
|
for (final var toBeMergedRecipientId : toBeMergedRecipientIds) {
|
||||||
if (isPniSignatureVerified(connection, toBeMergedRecipientId)) {
|
|
||||||
storePniSignatureVerified(connection, recipientId, true, true);
|
|
||||||
}
|
|
||||||
recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId);
|
recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId);
|
||||||
deleteRecipient(connection, toBeMergedRecipientId);
|
deleteRecipient(connection, toBeMergedRecipientId);
|
||||||
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId));
|
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId));
|
||||||
@ -1451,12 +1304,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET number = NULL,
|
SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
|
||||||
aci = NULL,
|
|
||||||
pni = NULL,
|
|
||||||
username = NULL,
|
|
||||||
storage_id = NULL,
|
|
||||||
pni_signature_verified = FALSE
|
|
||||||
WHERE _id = ?
|
WHERE _id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
@ -1472,18 +1320,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final RecipientAddress address
|
final RecipientAddress address
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
|
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
|
||||||
final var existingAddress = resolveRecipientAddress(connection, recipientId);
|
|
||||||
final var keepPniSignatureVerified = Objects.equals(existingAddress.aci(), address.aci()) && Objects.equals(
|
|
||||||
existingAddress.pni(),
|
|
||||||
address.pni());
|
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET number = ?,
|
SET number = ?, aci = ?, pni = ?, username = ?
|
||||||
aci = ?,
|
|
||||||
pni = ?,
|
|
||||||
username = ?,
|
|
||||||
pni_signature_verified = ?
|
|
||||||
WHERE _id = ?
|
WHERE _id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
@ -1492,8 +1332,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
statement.setString(2, address.aci().map(ACI::toString).orElse(null));
|
statement.setString(2, address.aci().map(ACI::toString).orElse(null));
|
||||||
statement.setString(3, address.pni().map(PNI::toString).orElse(null));
|
statement.setString(3, address.pni().map(PNI::toString).orElse(null));
|
||||||
statement.setString(4, address.username().orElse(null));
|
statement.setString(4, address.username().orElse(null));
|
||||||
statement.setBoolean(5, keepPniSignatureVerified && isPniSignatureVerified(connection, recipientId));
|
statement.setLong(5, recipientId.id());
|
||||||
statement.setLong(6, recipientId.id());
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
rotateStorageId(connection, recipientId);
|
rotateStorageId(connection, recipientId);
|
||||||
@ -1518,14 +1357,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
RecipientId toBeMergedRecipientId
|
RecipientId toBeMergedRecipientId
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var contact = getContact(connection, recipientId);
|
final var contact = getContact(connection, recipientId);
|
||||||
final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
|
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
|
final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
|
||||||
storeContact(connection, recipientId, toBeMergedContact);
|
storeContact(connection, recipientId, toBeMergedContact);
|
||||||
} else if (toBeMergedContact != null) {
|
|
||||||
final var mergedContact = mergeContacts(contact, toBeMergedContact);
|
|
||||||
if (!contact.equals(mergedContact)) {
|
|
||||||
storeContact(connection, recipientId, mergedContact);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final var profileKey = getProfileKey(connection, recipientId);
|
final var profileKey = getProfileKey(connection, recipientId);
|
||||||
@ -1550,24 +1384,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
|
recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
static Contact mergeContacts(final Contact primary, final Contact secondary) {
|
|
||||||
final var profileSharingEnabled = primary.isProfileSharingEnabled() || secondary.isProfileSharingEnabled();
|
|
||||||
return Contact.newBuilder(primary)
|
|
||||||
.withGivenName(secondary.givenName())
|
|
||||||
.withFamilyName(secondary.familyName())
|
|
||||||
.withMessageExpirationTime(primary.messageExpirationTime() > 0
|
|
||||||
? primary.messageExpirationTime()
|
|
||||||
: secondary.messageExpirationTime())
|
|
||||||
.withMessageExpirationTimeVersion(Math.max(primary.messageExpirationTimeVersion(),
|
|
||||||
secondary.messageExpirationTimeVersion()))
|
|
||||||
.withMuteUntil(primary.muteUntil() > 0 ? primary.muteUntil() : secondary.muteUntil())
|
|
||||||
.withIsBlocked(primary.isBlocked() || secondary.isBlocked())
|
|
||||||
.withBlockedAt(Math.max(primary.blockedAt(), secondary.blockedAt()))
|
|
||||||
.withIsProfileSharingEnabled(profileSharingEnabled)
|
|
||||||
.withIsHidden(profileSharingEnabled ? false : primary.isHidden())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<RecipientWithAddress> findByNumber(
|
private Optional<RecipientWithAddress> findByNumber(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final String number
|
final String number
|
||||||
@ -1647,7 +1463,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
|
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp
|
SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
|
||||||
FROM %s r
|
FROM %s r
|
||||||
WHERE r._id = ? AND (%s)
|
WHERE r._id = ? AND (%s)
|
||||||
"""
|
"""
|
||||||
@ -1734,7 +1550,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
getProfileFromResultSet(resultSet),
|
getProfileFromResultSet(resultSet),
|
||||||
getDiscoverableFromResultSet(resultSet),
|
getDiscoverableFromResultSet(resultSet),
|
||||||
getUnregisteredTimestampFromResultSet(resultSet),
|
getUnregisteredTimestampFromResultSet(resultSet),
|
||||||
resultSet.getBoolean("pni_signature_verified"),
|
|
||||||
getStorageRecordFromResultSet(resultSet));
|
getStorageRecordFromResultSet(resultSet));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1752,7 +1567,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
resultSet.getLong("mute_until"),
|
resultSet.getLong("mute_until"),
|
||||||
resultSet.getBoolean("hide_story"),
|
resultSet.getBoolean("hide_story"),
|
||||||
resultSet.getBoolean("blocked"),
|
resultSet.getBoolean("blocked"),
|
||||||
resultSet.getLong("blocked_at"),
|
|
||||||
resultSet.getBoolean("archived"),
|
resultSet.getBoolean("archived"),
|
||||||
resultSet.getBoolean("profile_sharing"),
|
resultSet.getBoolean("profile_sharing"),
|
||||||
resultSet.getBoolean("hidden"),
|
resultSet.getBoolean("hidden"),
|
||||||
@ -1791,9 +1605,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
profileCapabilities == null
|
profileCapabilities == null
|
||||||
? Set.of()
|
? Set.of()
|
||||||
: Arrays.stream(profileCapabilities.split(","))
|
: Arrays.stream(profileCapabilities.split(","))
|
||||||
.map(Profile.Capability::valueOfOrNull)
|
.map(Profile.Capability::valueOfOrNull)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toSet()),
|
.collect(Collectors.toSet()),
|
||||||
PhoneNumberSharingMode.valueOfOrNull(resultSet.getString("profile_phone_number_sharing")));
|
PhoneNumberSharingMode.valueOfOrNull(resultSet.getString("profile_phone_number_sharing")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -303,7 +303,6 @@ public class MessageSendLogStore implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
if (contentId == -1) {
|
if (contentId == -1) {
|
||||||
logger.warn("Failed to insert message send log content");
|
logger.warn("Failed to insert message send log content");
|
||||||
connection.commit();
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
insertRecipientsForExistingContent(contentId, recipientDevices, connection);
|
insertRecipientsForExistingContent(contentId, recipientDevices, connection);
|
||||||
@ -321,10 +320,10 @@ public class MessageSendLogStore implements AutoCloseable {
|
|||||||
return content.dataMessage == null
|
return content.dataMessage == null
|
||||||
? null
|
? null
|
||||||
: content.dataMessage.group != null && content.dataMessage.group.id != null
|
: content.dataMessage.group != null && content.dataMessage.group.id != null
|
||||||
? content.dataMessage.group.id.toByteArray()
|
? content.dataMessage.group.id.toByteArray()
|
||||||
: content.dataMessage.groupV2 != null && content.dataMessage.groupV2.masterKey != null
|
: content.dataMessage.groupV2 != null && content.dataMessage.groupV2.masterKey != null
|
||||||
? GroupUtils.getGroupIdV2(new GroupMasterKey(content.dataMessage.groupV2.masterKey.toByteArray()))
|
? GroupUtils.getGroupIdV2(new GroupMasterKey(content.dataMessage.groupV2.masterKey.toByteArray()))
|
||||||
.serialize()
|
.serialize()
|
||||||
: null;
|
: null;
|
||||||
} catch (InvalidInputException e) {
|
} catch (InvalidInputException e) {
|
||||||
logger.warn("Failed to parse groupId id from content");
|
logger.warn("Failed to parse groupId id from content");
|
||||||
|
|||||||
@ -204,7 +204,7 @@ public class SenderKeySharedStore {
|
|||||||
).formatted(TABLE_SENDER_KEY_SHARED);
|
).formatted(TABLE_SENDER_KEY_SHARED);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
for (final var entry : newEntries) {
|
for (final var entry : newEntries) {
|
||||||
statement.setString(1, entry.address());
|
statement.setString(1, entry.toString());
|
||||||
statement.setInt(2, entry.deviceId());
|
statement.setInt(2, entry.deviceId());
|
||||||
statement.setBytes(3, UuidUtil.toByteArray(distributionId.asUuid()));
|
statement.setBytes(3, UuidUtil.toByteArray(distributionId.asUuid()));
|
||||||
statement.setLong(4, System.currentTimeMillis());
|
statement.setLong(4, System.currentTimeMillis());
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import java.sql.ResultSet;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -28,15 +28,8 @@ public class SessionStore implements SignalServiceSessionStore {
|
|||||||
|
|
||||||
private static final String TABLE_SESSION = "session";
|
private static final String TABLE_SESSION = "session";
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SessionStore.class);
|
private static final Logger logger = LoggerFactory.getLogger(SessionStore.class);
|
||||||
private static final int MAX_CACHE_SIZE = 1000;
|
|
||||||
|
|
||||||
private final Map<Key, SessionRecord> cachedSessions = new LinkedHashMap<>(16, 0.75f, true) {
|
private final Map<Key, SessionRecord> cachedSessions = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean removeEldestEntry(Map.Entry<Key, SessionRecord> eldest) {
|
|
||||||
return size() > MAX_CACHE_SIZE;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Database database;
|
private final Database database;
|
||||||
private final int accountIdType;
|
private final int accountIdType;
|
||||||
@ -201,9 +194,8 @@ public class SessionStore implements SignalServiceSessionStore {
|
|||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.archiveCurrentState();
|
session.archiveCurrentState();
|
||||||
storeSession(connection, key, session);
|
storeSession(connection, key, session);
|
||||||
|
connection.commit();
|
||||||
}
|
}
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException("Failed update session store", e);
|
throw new RuntimeException("Failed update session store", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,10 @@
|
|||||||
package org.asamk.signal.manager.storage.stickers;
|
package org.asamk.signal.manager.storage.stickers;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.StickerPackId;
|
import org.asamk.signal.manager.api.StickerPackId;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
|
||||||
|
|
||||||
public record StickerPack(
|
public record StickerPack(long internalId, StickerPackId packId, byte[] packKey, boolean isInstalled) {
|
||||||
long internalId,
|
|
||||||
StickerPackId packId,
|
|
||||||
byte[] packKey,
|
|
||||||
boolean isInstalled,
|
|
||||||
int position,
|
|
||||||
long deletedTimestamp,
|
|
||||||
StorageId storageId,
|
|
||||||
byte[] storageRecord
|
|
||||||
) {
|
|
||||||
|
|
||||||
public StickerPack(
|
|
||||||
final long internalId,
|
|
||||||
final StickerPackId packId,
|
|
||||||
final byte[] packKey,
|
|
||||||
final boolean isInstalled
|
|
||||||
) {
|
|
||||||
this(internalId, packId, packKey, isInstalled, 0, 0, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StickerPack(final StickerPackId packId, final byte[] packKey) {
|
public StickerPack(final StickerPackId packId, final byte[] packKey) {
|
||||||
this(-1, packId, packKey, false, 0, 0, null, null);
|
this(-1, packId, packKey, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,19 +3,14 @@ package org.asamk.signal.manager.storage.stickers;
|
|||||||
import org.asamk.signal.manager.api.StickerPackId;
|
import org.asamk.signal.manager.api.StickerPackId;
|
||||||
import org.asamk.signal.manager.storage.Database;
|
import org.asamk.signal.manager.storage.Database;
|
||||||
import org.asamk.signal.manager.storage.Utils;
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Types;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class StickerStore {
|
public class StickerStore {
|
||||||
|
|
||||||
@ -32,11 +27,7 @@ public class StickerStore {
|
|||||||
_id INTEGER PRIMARY KEY,
|
_id INTEGER PRIMARY KEY,
|
||||||
pack_id BLOB UNIQUE NOT NULL,
|
pack_id BLOB UNIQUE NOT NULL,
|
||||||
pack_key BLOB NOT NULL,
|
pack_key BLOB NOT NULL,
|
||||||
installed INTEGER NOT NULL DEFAULT FALSE,
|
installed INTEGER NOT NULL DEFAULT FALSE
|
||||||
position INTEGER NOT NULL DEFAULT 0,
|
|
||||||
deleted_timestamp INTEGER NOT NULL DEFAULT 0,
|
|
||||||
storage_id BLOB UNIQUE,
|
|
||||||
storage_record BLOB
|
|
||||||
) STRICT;
|
) STRICT;
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
@ -47,103 +38,55 @@ public class StickerStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<StickerPack> getStickerPacks() {
|
public List<StickerPack> getStickerPacks() {
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
return getStickerPacks(connection);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed read from sticker store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<StickerPack> getStickerPacks(final Connection connection) throws SQLException {
|
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT s._id, s.pack_id, s.pack_key, s.installed, s.position, s.deleted_timestamp, s.storage_id, s.storage_record
|
SELECT s._id, s.pack_id, s.pack_key, s.installed
|
||||||
FROM %s s
|
FROM %s s
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var connection = database.getConnection()) {
|
||||||
try (var result = Utils.executeQueryForStream(statement, this::getStickerPackFromResultSet)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
return result.toList();
|
try (var result = Utils.executeQueryForStream(statement, this::getStickerPackFromResultSet)) {
|
||||||
|
return result.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed read from sticker store", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public StickerPack getStickerPack(StickerPackId packId) {
|
public StickerPack getStickerPack(StickerPackId packId) {
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
return getStickerPack(connection, packId);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed read from sticker store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public StickerPack getStickerPack(Connection connection, StickerPackId packId) throws SQLException {
|
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT s._id, s.pack_id, s.pack_key, s.installed, s.position, s.deleted_timestamp, s.storage_id, s.storage_record
|
SELECT s._id, s.pack_id, s.pack_key, s.installed
|
||||||
FROM %s s
|
FROM %s s
|
||||||
WHERE s.pack_id = ?
|
WHERE s.pack_id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var connection = database.getConnection()) {
|
||||||
statement.setBytes(1, packId.serialize());
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
statement.setBytes(1, packId.serialize());
|
||||||
}
|
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
||||||
}
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
public StickerPack getStickerPack(Connection connection, StorageId storageId) throws SQLException {
|
throw new RuntimeException("Failed read from sticker store", e);
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT s._id, s.pack_id, s.pack_key, s.installed, s.position, s.deleted_timestamp, s.storage_id, s.storage_record
|
|
||||||
FROM %s s
|
|
||||||
WHERE s.storage_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addStickerPack(StickerPack stickerPack) {
|
public void addStickerPack(StickerPack stickerPack) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
INSERT INTO %s (pack_id, pack_key, installed)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
connection.setAutoCommit(false);
|
|
||||||
var storageId = stickerPack.storageId();
|
|
||||||
if (storageId == null && (stickerPack.isInstalled() || stickerPack.deletedTimestamp() > 0)) {
|
|
||||||
storageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
|
||||||
}
|
|
||||||
|
|
||||||
final var position = stickerPack.isInstalled() ? Math.max(stickerPack.position(),
|
|
||||||
getNextPosition(connection)) : 0;
|
|
||||||
var deletedTimestamp = stickerPack.deletedTimestamp();
|
|
||||||
if (!stickerPack.isInstalled() && deletedTimestamp == 0 && storageId != null) {
|
|
||||||
deletedTimestamp = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setBytes(1, stickerPack.packId().serialize());
|
statement.setBytes(1, stickerPack.packId().serialize());
|
||||||
statement.setBytes(2, stickerPack.packKey());
|
statement.setBytes(2, stickerPack.packKey());
|
||||||
statement.setBoolean(3, stickerPack.isInstalled());
|
statement.setBoolean(3, stickerPack.isInstalled());
|
||||||
statement.setInt(4, position);
|
|
||||||
statement.setLong(5, deletedTimestamp);
|
|
||||||
if (storageId == null) {
|
|
||||||
statement.setNull(6, Types.BLOB);
|
|
||||||
} else {
|
|
||||||
statement.setBytes(6, storageId.getRaw());
|
|
||||||
}
|
|
||||||
if (stickerPack.storageRecord() == null) {
|
|
||||||
statement.setNull(7, Types.BLOB);
|
|
||||||
} else {
|
|
||||||
statement.setBytes(7, stickerPack.storageRecord());
|
|
||||||
}
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException("Failed update sticker store", e);
|
throw new RuntimeException("Failed update sticker store", e);
|
||||||
}
|
}
|
||||||
@ -153,279 +96,28 @@ public class StickerStore {
|
|||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?
|
SET installed = ?
|
||||||
WHERE pack_id = ?
|
WHERE pack_id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
connection.setAutoCommit(false);
|
|
||||||
final var existing = getStickerPack(connection, stickerPackId);
|
|
||||||
if (existing == null || existing.isInstalled() == installed) {
|
|
||||||
connection.commit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var newStorageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
|
||||||
final var position = installed ? getNextPosition(connection) : 0;
|
|
||||||
final var deletedTimestamp = installed ? 0 : System.currentTimeMillis();
|
|
||||||
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setBoolean(1, installed);
|
statement.setBytes(1, stickerPackId.serialize());
|
||||||
statement.setInt(2, position);
|
statement.setBoolean(2, installed);
|
||||||
statement.setLong(3, deletedTimestamp);
|
|
||||||
statement.setBytes(4, newStorageId.getRaw());
|
|
||||||
statement.setBytes(5, stickerPackId.serialize());
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException("Failed update sticker store", e);
|
throw new RuntimeException("Failed update sticker store", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<StorageId> getStorageIds(final Connection connection) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT s.storage_id
|
|
||||||
FROM %s s
|
|
||||||
WHERE s.storage_id IS NOT NULL
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
return Utils.executeQueryForStream(statement, this::getStorageIdFromResultSet).toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateStorageId(
|
|
||||||
final Connection connection,
|
|
||||||
final StickerPackId packId,
|
|
||||||
final StorageId storageId
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
statement.setBytes(2, packId.serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateStorageIds(
|
|
||||||
final Connection connection,
|
|
||||||
final Map<StickerPackId, StorageId> storageIdMap
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
for (final var entry : storageIdMap.entrySet()) {
|
|
||||||
statement.setBytes(1, entry.getValue().getRaw());
|
|
||||||
statement.setBytes(2, entry.getKey().serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public StorageId getStorageId(final Connection connection, final StickerPackId packId) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT s.storage_id
|
|
||||||
FROM %s s
|
|
||||||
WHERE s.pack_id = ? AND s.storage_id IS NOT NULL
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setBytes(1, packId.serialize());
|
|
||||||
final var storageId = Utils.executeQueryForOptional(statement, this::getStorageIdFromResultSet);
|
|
||||||
if (storageId.isPresent()) {
|
|
||||||
return storageId.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final var newStorageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
|
||||||
updateStorageId(connection, packId, newStorageId);
|
|
||||||
return newStorageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void storeStorageRecord(
|
|
||||||
final Connection connection,
|
|
||||||
final StickerPackId packId,
|
|
||||||
final StorageId storageId,
|
|
||||||
final byte[] storageRecord
|
|
||||||
) throws SQLException {
|
|
||||||
final var clearSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = NULL
|
|
||||||
WHERE storage_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(clearSql)) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
final var updateSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = ?, storage_record = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
if (storageRecord == null) {
|
|
||||||
statement.setNull(2, Types.BLOB);
|
|
||||||
} else {
|
|
||||||
statement.setBytes(2, storageRecord);
|
|
||||||
}
|
|
||||||
statement.setBytes(3, packId.serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMissingStorageIds() {
|
|
||||||
final var selectSql = (
|
|
||||||
"""
|
|
||||||
SELECT s.pack_id
|
|
||||||
FROM %s s
|
|
||||||
WHERE s.storage_id IS NULL AND s.installed = TRUE
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
final var updateSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
|
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
connection.setAutoCommit(false);
|
|
||||||
try (final var selectStatement = connection.prepareStatement(selectSql)) {
|
|
||||||
final var packIds = Utils.executeQueryForStream(selectStatement,
|
|
||||||
resultSet -> StickerPackId.deserialize(resultSet.getBytes("pack_id"))).toList();
|
|
||||||
try (final var updateStatement = connection.prepareStatement(updateSql)) {
|
|
||||||
for (final var packId : packIds) {
|
|
||||||
updateStatement.setBytes(1, KeyUtils.createRawStorageId());
|
|
||||||
updateStatement.setBytes(2, packId.serialize());
|
|
||||||
updateStatement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed update sticker store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int removeStorageIdsFromLocalOnlyDeletedStickerPacks(
|
|
||||||
final Connection connection,
|
|
||||||
final Collection<StorageId> storageIds
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = NULL
|
|
||||||
WHERE storage_id = ? AND installed = FALSE AND deleted_timestamp > 0
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
var count = 0;
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
for (final var storageId : storageIds) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
count += statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upsertFromStorageSync(
|
|
||||||
final Connection connection,
|
|
||||||
final SignalStickerPackRecord record
|
|
||||||
) throws SQLException {
|
|
||||||
final var remote = record.getProto();
|
|
||||||
final var packId = StickerPackId.deserialize(remote.packId.toByteArray());
|
|
||||||
final var deleted = remote.deletedAtTimestamp > 0;
|
|
||||||
final var packKey = remote.packKey.toByteArray();
|
|
||||||
final var storageRecord = remote.encode();
|
|
||||||
|
|
||||||
final var current = getStickerPack(connection, packId);
|
|
||||||
|
|
||||||
if (current == null) {
|
|
||||||
final var insertSql = (
|
|
||||||
"""
|
|
||||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(insertSql)) {
|
|
||||||
statement.setBytes(1, packId.serialize());
|
|
||||||
statement.setBytes(2, packKey);
|
|
||||||
statement.setBoolean(3, !deleted);
|
|
||||||
statement.setInt(4, deleted ? 0 : remote.position);
|
|
||||||
statement.setLong(5, remote.deletedAtTimestamp);
|
|
||||||
statement.setBytes(6, record.getId().getRaw());
|
|
||||||
statement.setBytes(7, storageRecord);
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packKey.length > 0) {
|
|
||||||
final var updateSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET pack_key = ?, installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?, storage_record = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
|
||||||
statement.setBytes(1, packKey);
|
|
||||||
statement.setBoolean(2, !deleted);
|
|
||||||
statement.setInt(3, deleted ? 0 : remote.position);
|
|
||||||
statement.setLong(4, remote.deletedAtTimestamp);
|
|
||||||
statement.setBytes(5, record.getId().getRaw());
|
|
||||||
statement.setBytes(6, storageRecord);
|
|
||||||
statement.setBytes(7, packId.serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final var updateSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?, storage_record = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
|
||||||
statement.setBoolean(1, !deleted);
|
|
||||||
statement.setInt(2, deleted ? 0 : remote.position);
|
|
||||||
statement.setLong(3, remote.deletedAtTimestamp);
|
|
||||||
statement.setBytes(4, record.getId().getRaw());
|
|
||||||
statement.setBytes(5, storageRecord);
|
|
||||||
statement.setBytes(6, packId.serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addLegacyStickers(Collection<StickerPack> stickerPacks) {
|
void addLegacyStickers(Collection<StickerPack> stickerPacks) {
|
||||||
logger.debug("Migrating legacy stickers to database");
|
logger.debug("Migrating legacy stickers to database");
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
INSERT INTO %s (pack_id, pack_key, installed)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
@ -433,23 +125,11 @@ public class StickerStore {
|
|||||||
try (final var statement = connection.prepareStatement("DELETE FROM %s".formatted(TABLE_STICKER))) {
|
try (final var statement = connection.prepareStatement("DELETE FROM %s".formatted(TABLE_STICKER))) {
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
var installedPosition = 0;
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
for (final var sticker : stickerPacks) {
|
for (final var sticker : stickerPacks) {
|
||||||
final var storageId = sticker.isInstalled()
|
|
||||||
? StorageId.forStickerPack(KeyUtils.createRawStorageId())
|
|
||||||
: null;
|
|
||||||
statement.setBytes(1, sticker.packId().serialize());
|
statement.setBytes(1, sticker.packId().serialize());
|
||||||
statement.setBytes(2, sticker.packKey());
|
statement.setBytes(2, sticker.packKey());
|
||||||
statement.setBoolean(3, sticker.isInstalled());
|
statement.setBoolean(3, sticker.isInstalled());
|
||||||
statement.setInt(4, sticker.isInstalled() ? installedPosition++ : 0);
|
|
||||||
statement.setLong(5, 0);
|
|
||||||
if (storageId == null) {
|
|
||||||
statement.setNull(6, Types.BLOB);
|
|
||||||
} else {
|
|
||||||
statement.setBytes(6, storageId.getRaw());
|
|
||||||
}
|
|
||||||
statement.setNull(7, Types.BLOB);
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,36 +145,6 @@ public class StickerStore {
|
|||||||
final var packId = resultSet.getBytes("pack_id");
|
final var packId = resultSet.getBytes("pack_id");
|
||||||
final var packKey = resultSet.getBytes("pack_key");
|
final var packKey = resultSet.getBytes("pack_key");
|
||||||
final var installed = resultSet.getBoolean("installed");
|
final var installed = resultSet.getBoolean("installed");
|
||||||
final var position = resultSet.getInt("position");
|
return new StickerPack(internalId, StickerPackId.deserialize(packId), packKey, installed);
|
||||||
final var deletedTimestamp = resultSet.getLong("deleted_timestamp");
|
|
||||||
final var storageIdBytes = resultSet.getBytes("storage_id");
|
|
||||||
final var storageId = storageIdBytes == null ? null : StorageId.forStickerPack(storageIdBytes);
|
|
||||||
final var storageRecord = resultSet.getBytes("storage_record");
|
|
||||||
return new StickerPack(internalId,
|
|
||||||
StickerPackId.deserialize(packId),
|
|
||||||
packKey,
|
|
||||||
installed,
|
|
||||||
position,
|
|
||||||
deletedTimestamp,
|
|
||||||
storageId,
|
|
||||||
storageRecord);
|
|
||||||
}
|
|
||||||
|
|
||||||
private StorageId getStorageIdFromResultSet(final ResultSet resultSet) throws SQLException {
|
|
||||||
final var storageId = resultSet.getBytes("storage_id");
|
|
||||||
return StorageId.forStickerPack(storageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getNextPosition(final Connection connection) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT IFNULL(MAX(position) + 1, 0) AS next_position
|
|
||||||
FROM %s
|
|
||||||
WHERE installed = TRUE
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
return Utils.executeQuerySingleRow(statement, resultSet -> resultSet.getInt("next_position"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id
|
|||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@ -57,29 +56,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
this.selfNumber = account.getNumber();
|
this.selfNumber = account.getNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void prepare(final Collection<SignalContactRecord> remoteRecords) throws SQLException {
|
|
||||||
for (final var remoteRecord : remoteRecords) {
|
|
||||||
if (isInvalid(remoteRecord)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final var remote = remoteRecord.getProto();
|
|
||||||
final var aci = ACI.parseOrNull(remote.aci, remote.aciBinary);
|
|
||||||
final var pni = PNI.parseOrNull(remote.pni, remote.pniBinary);
|
|
||||||
if (shouldSplitForStorageSync(remote.unregisteredAtTimestamp, aci, pni, remote.e164)) {
|
|
||||||
account.getRecipientStore().splitForStorageSyncIfNecessary(connection, aci);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean shouldSplitForStorageSync(
|
|
||||||
final long unregisteredAtTimestamp,
|
|
||||||
final ACI aci,
|
|
||||||
final PNI pni,
|
|
||||||
final String e164
|
|
||||||
) {
|
|
||||||
return unregisteredAtTimestamp > 0 && aci != null && pni == null && e164.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error cases:
|
* Error cases:
|
||||||
* - You can't have a contact record without an ACI or PNI.
|
* - You can't have a contact record without an ACI or PNI.
|
||||||
@ -141,35 +117,24 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
|
|
||||||
IdentityState identityState;
|
IdentityState identityState;
|
||||||
ByteString identityKey;
|
ByteString identityKey;
|
||||||
|
if (remote.identityKey.size() > 0 && (
|
||||||
|
!account.isPrimaryDevice()
|
||||||
|
|| remote.identityState != local.identityState
|
||||||
|
|| local.identityKey.size() == 0
|
||||||
|
|
||||||
// Parse ACI/PNI first so we can determine if contact has identity
|
)) {
|
||||||
final var localAci = ACI.parseOrNull(local.aci, local.aciBinary);
|
|
||||||
final var localPni = PNI.parseOrNull(local.pni, local.pniBinary);
|
|
||||||
final var remoteAci = ACI.parseOrNull(remote.aci, remote.aciBinary);
|
|
||||||
final var remotePni = PNI.parseOrNull(remote.pni, remote.pniBinary);
|
|
||||||
|
|
||||||
final var hasLocalIdentity = localAci != null || localPni != null;
|
|
||||||
final var hasRemoteIdentity = remoteAci != null || remotePni != null;
|
|
||||||
|
|
||||||
final var remoteIdentityKeySize = remote.identityKey.size();
|
|
||||||
final var localIdentityKeySize = local.identityKey.size();
|
|
||||||
final var statesDiffer = remote.identityState != local.identityState;
|
|
||||||
|
|
||||||
if (remoteIdentityKeySize > 0 && (!account.isPrimaryDevice() || statesDiffer || localIdentityKeySize == 0)) {
|
|
||||||
identityState = remote.identityState;
|
identityState = remote.identityState;
|
||||||
identityKey = remote.identityKey;
|
identityKey = remote.identityKey;
|
||||||
} else {
|
} else {
|
||||||
identityState = local.identityState;
|
identityState = local.identityState;
|
||||||
// Only use local's identity key if:
|
identityKey = local.identityKey.size() > 0 ? local.identityKey : ByteString.EMPTY;
|
||||||
// 1. Contact has ACI or PNI
|
|
||||||
// 2. Remote also has an identity key (if remote size=0, respect that decision)
|
|
||||||
if (hasLocalIdentity && localIdentityKeySize > 0 && remoteIdentityKeySize > 0) {
|
|
||||||
identityKey = local.identityKey;
|
|
||||||
} else {
|
|
||||||
identityKey = ByteString.EMPTY;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final var localAci = ACI.parseOrNull(local.aci, local.aciBinary);
|
||||||
|
final var localPni = PNI.parseOrNull(local.pni, local.pniBinary);
|
||||||
|
final var remoteAci = ACI.parseOrNull(remote.aci, remote.aciBinary);
|
||||||
|
final var remotePni = PNI.parseOrNull(remote.pni, remote.pniBinary);
|
||||||
|
|
||||||
if (localAci != null
|
if (localAci != null
|
||||||
&& local.identityKey.size() > 0
|
&& local.identityKey.size() > 0
|
||||||
&& remote.identityKey.size() > 0
|
&& remote.identityKey.size() > 0
|
||||||
@ -229,7 +194,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
.identityState(identityState)
|
.identityState(identityState)
|
||||||
.identityKey(identityKey)
|
.identityKey(identityKey)
|
||||||
.blocked(remote.blocked)
|
.blocked(remote.blocked)
|
||||||
.blockedAtTimestamp(remote.blockedAtTimestamp)
|
|
||||||
.whitelisted(remote.whitelisted)
|
.whitelisted(remote.whitelisted)
|
||||||
.archived(remote.archived)
|
.archived(remote.archived)
|
||||||
.markedUnread(remote.markedUnread)
|
.markedUnread(remote.markedUnread)
|
||||||
@ -308,9 +272,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
|
final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
|
||||||
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
|
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
|
||||||
final var contactNote = contact == null ? null : contact.note();
|
final var contactNote = contact == null ? null : contact.note();
|
||||||
final var blockedAt = contact == null ? 0 : contact.blockedAt();
|
|
||||||
if (blocked != contactProto.blocked
|
if (blocked != contactProto.blocked
|
||||||
|| blockedAt != contactProto.blockedAtTimestamp
|
|
||||||
|| profileShared != contactProto.whitelisted
|
|| profileShared != contactProto.whitelisted
|
||||||
|| archived != contactProto.archived
|
|| archived != contactProto.archived
|
||||||
|| hidden != contactProto.hidden
|
|| hidden != contactProto.hidden
|
||||||
@ -328,7 +290,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
logger.debug("Storing new or updated contact {}", recipientId);
|
logger.debug("Storing new or updated contact {}", recipientId);
|
||||||
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
|
final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
|
||||||
.withBlockedAt(contactProto.blocked ? contactProto.blockedAtTimestamp : 0)
|
|
||||||
.withIsProfileSharingEnabled(contactProto.whitelisted)
|
.withIsProfileSharingEnabled(contactProto.whitelisted)
|
||||||
.withIsArchived(contactProto.archived)
|
.withIsArchived(contactProto.archived)
|
||||||
.withIsHidden(contactProto.hidden)
|
.withIsHidden(contactProto.hidden)
|
||||||
@ -385,8 +346,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
logger.warn("Received invalid contact identity key from storage");
|
logger.warn("Received invalid contact identity key from storage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
account.getRecipientStore()
|
|
||||||
.storePniSignatureVerified(connection, recipientId, contactProto.pniSignatureVerified);
|
|
||||||
account.getRecipientStore()
|
account.getRecipientStore()
|
||||||
.storeStorageRecord(connection, recipientId, contactRecord.getId(), contactProto.encode());
|
.storeStorageRecord(connection, recipientId, contactRecord.getId(), contactProto.encode());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,7 @@ import org.whispersystems.signalservice.api.storage.SignalRecord;
|
|||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
@ -26,7 +24,6 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DefaultStorageRecordProcessor.class);
|
private static final Logger logger = LoggerFactory.getLogger(DefaultStorageRecordProcessor.class);
|
||||||
private final Set<E> matchedRecords = new TreeSet<>(this);
|
private final Set<E> matchedRecords = new TreeSet<>(this);
|
||||||
private final Set<StorageId> updatedStorageIds = new HashSet<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One type of invalid remote data this handles is two records mapping to the same local data. We
|
* One type of invalid remote data this handles is two records mapping to the same local data. We
|
||||||
@ -53,7 +50,6 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
|
|||||||
|
|
||||||
if (local.isEmpty()) {
|
if (local.isEmpty()) {
|
||||||
debug(remote.getId(), remote, "[Local Insert] No matching local record. Inserting.");
|
debug(remote.getId(), remote, "[Local Insert] No matching local record. Inserting.");
|
||||||
updatedStorageIds.add(remote.getId());
|
|
||||||
insertLocal(remote);
|
insertLocal(remote);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -68,7 +64,6 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
|
|||||||
matchedRecords.add(local.get());
|
matchedRecords.add(local.get());
|
||||||
|
|
||||||
final var merged = merge(remote, local.get());
|
final var merged = merge(remote, local.get());
|
||||||
updatedStorageIds.add(merged.getId());
|
|
||||||
if (!merged.equals(remote)) {
|
if (!merged.equals(remote)) {
|
||||||
debug(remote.getId(), remote, "[Remote Update] " + merged.describeDiff(remote));
|
debug(remote.getId(), remote, "[Remote Update] " + merged.describeDiff(remote));
|
||||||
}
|
}
|
||||||
@ -80,10 +75,6 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<StorageId> getUpdatedStorageIds() {
|
|
||||||
return Collections.unmodifiableSet(updatedStorageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void debug(StorageId i, E record, String message) {
|
private void debug(StorageId i, E record, String message) {
|
||||||
logger.debug("[{}][{}] {}", i, record.getClass().getSimpleName(), message);
|
logger.debug("[{}][{}] {}", i, record.getClass().getSimpleName(), message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,7 +114,6 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||||||
final var group = account.getGroupStore().getOrCreateGroupV1(connection, groupIdV1);
|
final var group = account.getGroupStore().getOrCreateGroupV1(connection, groupIdV1);
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
group.setBlocked(groupV1Proto.blocked);
|
group.setBlocked(groupV1Proto.blocked);
|
||||||
group.setBlockedAt(0);
|
|
||||||
account.getGroupStore().updateGroup(connection, group);
|
account.getGroupStore().updateGroup(connection, group);
|
||||||
account.getGroupStore()
|
account.getGroupStore()
|
||||||
.storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
|
.storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
|
||||||
|
|||||||
@ -56,7 +56,6 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||||||
final var mergedBuilder = remote.newBuilder()
|
final var mergedBuilder = remote.newBuilder()
|
||||||
.masterKey(remote.masterKey)
|
.masterKey(remote.masterKey)
|
||||||
.blocked(remote.blocked)
|
.blocked(remote.blocked)
|
||||||
.blockedAtTimestamp(remote.blockedAtTimestamp)
|
|
||||||
.whitelisted(remote.whitelisted)
|
.whitelisted(remote.whitelisted)
|
||||||
.archived(remote.archived)
|
.archived(remote.archived)
|
||||||
.markedUnread(remote.markedUnread)
|
.markedUnread(remote.markedUnread)
|
||||||
@ -94,7 +93,6 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||||||
|
|
||||||
final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
|
final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
|
||||||
group.setBlocked(groupV2Proto.blocked);
|
group.setBlocked(groupV2Proto.blocked);
|
||||||
group.setBlockedAt(groupV2Proto.blocked ? groupV2Proto.blockedAtTimestamp : 0);
|
|
||||||
group.setProfileSharingEnabled(groupV2Proto.whitelisted);
|
group.setProfileSharingEnabled(groupV2Proto.whitelisted);
|
||||||
account.getGroupStore().updateGroup(connection, group);
|
account.getGroupStore().updateGroup(connection, group);
|
||||||
account.getGroupStore()
|
account.getGroupStore()
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
package org.asamk.signal.manager.syncStorage;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.StickerPackId;
|
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class StickerPackRecordProcessor extends DefaultStorageRecordProcessor<SignalStickerPackRecord> {
|
|
||||||
|
|
||||||
private static final int PACK_ID_LENGTH = 16;
|
|
||||||
private static final int PACK_KEY_LENGTH = 32;
|
|
||||||
|
|
||||||
private final SignalAccount account;
|
|
||||||
private final Connection connection;
|
|
||||||
|
|
||||||
public StickerPackRecordProcessor(final SignalAccount account, final Connection connection) {
|
|
||||||
this.account = account;
|
|
||||||
this.connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compare(final SignalStickerPackRecord lhs, final SignalStickerPackRecord rhs) {
|
|
||||||
return lhs.getProto().packId.equals(rhs.getProto().packId) ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isInvalid(final SignalStickerPackRecord remote) {
|
|
||||||
return remote.getProto().packId.size() != PACK_ID_LENGTH || (
|
|
||||||
remote.getProto().deletedAtTimestamp == 0 && remote.getProto().packKey.size() != PACK_KEY_LENGTH
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Optional<SignalStickerPackRecord> getMatching(final SignalStickerPackRecord remote) throws SQLException {
|
|
||||||
final var packId = StickerPackId.deserialize(remote.getProto().packId.toByteArray());
|
|
||||||
final var local = account.getStickerStore().getStickerPack(connection, packId);
|
|
||||||
|
|
||||||
if (local == null || (!local.isInstalled() && local.deletedTimestamp() == 0)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
final StorageId storageId;
|
|
||||||
if (local.storageId() != null) {
|
|
||||||
storageId = local.storageId();
|
|
||||||
} else {
|
|
||||||
storageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
|
||||||
account.getStickerStore().updateStorageId(connection, packId, storageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.of(new SignalStickerPackRecord(storageId, StorageSyncModels.localToRemoteRecord(local)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected SignalStickerPackRecord merge(
|
|
||||||
final SignalStickerPackRecord remoteRecord,
|
|
||||||
final SignalStickerPackRecord localRecord
|
|
||||||
) {
|
|
||||||
final var remote = remoteRecord.getProto();
|
|
||||||
final var local = localRecord.getProto();
|
|
||||||
|
|
||||||
if (shouldKeepLocalDeletion(remote.deletedAtTimestamp, local.deletedAtTimestamp)) {
|
|
||||||
return localRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
return remoteRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean shouldKeepLocalDeletion(final long remoteDeletedAt, final long localDeletedAt) {
|
|
||||||
return remoteDeletedAt > 0 && localDeletedAt > 0 && localDeletedAt < remoteDeletedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void insertLocal(final SignalStickerPackRecord record) throws SQLException {
|
|
||||||
account.getStickerStore().upsertFromStorageSync(connection, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void updateLocal(final StorageRecordUpdate<SignalStickerPackRecord> update) throws SQLException {
|
|
||||||
account.getStickerStore().upsertFromStorageSync(connection, update.newRecord());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
package org.asamk.signal.manager.syncStorage;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.util.LeakyBucket;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.BooleanSupplier;
|
|
||||||
|
|
||||||
public final class StorageSyncLoopDetector {
|
|
||||||
|
|
||||||
private static final int FINGERPRINT_HISTORY = 3;
|
|
||||||
|
|
||||||
private final BooleanSupplier isMultiDevice;
|
|
||||||
private final List<Integer> recentFingerprints = new ArrayList<>();
|
|
||||||
private final LeakyBucket contentBucket = new LeakyBucket(3,
|
|
||||||
Duration.ofHours(1).toMillis(),
|
|
||||||
new InMemoryBucketState());
|
|
||||||
private final LeakyBucket rateBucket = new LeakyBucket(100,
|
|
||||||
Duration.ofMinutes(10).toMillis(),
|
|
||||||
new InMemoryBucketState());
|
|
||||||
|
|
||||||
public StorageSyncLoopDetector(final BooleanSupplier isMultiDevice) {
|
|
||||||
this.isMultiDevice = isMultiDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized Decision onWriteAttempt(
|
|
||||||
final WriteOperationResult write,
|
|
||||||
final boolean fetchedRemoteManifest,
|
|
||||||
final boolean isRetry
|
|
||||||
) {
|
|
||||||
return onWriteAttempt(write, fetchedRemoteManifest, isRetry, System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized Decision onWriteAttempt(
|
|
||||||
final WriteOperationResult write,
|
|
||||||
final boolean fetchedRemoteManifest,
|
|
||||||
final boolean isRetry,
|
|
||||||
final long now
|
|
||||||
) {
|
|
||||||
if (!isMultiDevice.getAsBoolean() || isRetry) {
|
|
||||||
return Decision.Allowed.INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var fingerprint = fingerprint(write);
|
|
||||||
final var chargeContent = fetchedRemoteManifest && fingerprint != null && recentFingerprints.contains(
|
|
||||||
fingerprint);
|
|
||||||
|
|
||||||
if (chargeContent && !contentBucket.hasRoom(now)) {
|
|
||||||
return new Decision.Denied(Cause.REPEATED_PAYLOAD, contentBucket.level(now));
|
|
||||||
}
|
|
||||||
if (fetchedRemoteManifest && !rateBucket.hasRoom(now)) {
|
|
||||||
return new Decision.Denied(Cause.WRITE_RATE, rateBucket.level(now));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chargeContent) {
|
|
||||||
contentBucket.use(now);
|
|
||||||
}
|
|
||||||
if (fetchedRemoteManifest) {
|
|
||||||
rateBucket.use(now);
|
|
||||||
}
|
|
||||||
if (fingerprint != null) {
|
|
||||||
remember(fingerprint);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Decision.Allowed.INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void onWriteFailed() {
|
|
||||||
onWriteFailed(System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void onWriteFailed(final long now) {
|
|
||||||
contentBucket.refund(now);
|
|
||||||
rateBucket.refund(now);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void onConverged() {
|
|
||||||
contentBucket.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void remember(final int fingerprint) {
|
|
||||||
recentFingerprints.remove(Integer.valueOf(fingerprint));
|
|
||||||
recentFingerprints.addFirst(fingerprint);
|
|
||||||
if (recentFingerprints.size() > FINGERPRINT_HISTORY) {
|
|
||||||
recentFingerprints.removeLast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Integer fingerprint(final WriteOperationResult write) {
|
|
||||||
if (write.inserts().isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return write.inserts()
|
|
||||||
.stream()
|
|
||||||
.map(record -> Arrays.hashCode(record.getProto().encode()))
|
|
||||||
.sorted()
|
|
||||||
.toList()
|
|
||||||
.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class InMemoryBucketState implements LeakyBucket.State {
|
|
||||||
|
|
||||||
private int level;
|
|
||||||
private long levelUpdatedAt;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int level() {
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long levelUpdatedAt() {
|
|
||||||
return levelUpdatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void update(final int level, final long levelUpdatedAt) {
|
|
||||||
this.level = level;
|
|
||||||
this.levelUpdatedAt = levelUpdatedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Cause {
|
|
||||||
REPEATED_PAYLOAD,
|
|
||||||
WRITE_RATE
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed interface Decision {
|
|
||||||
|
|
||||||
enum Allowed implements Decision {
|
|
||||||
INSTANCE
|
|
||||||
}
|
|
||||||
|
|
||||||
record Denied(Cause cause, int level) implements Decision {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,7 +8,6 @@ import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
|||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
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.Recipient;
|
import org.asamk.signal.manager.storage.recipients.Recipient;
|
||||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
import org.signal.core.util.UuidUtil;
|
import org.signal.core.util.UuidUtil;
|
||||||
@ -17,14 +16,12 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
|||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink;
|
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.StickerPackRecord;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@ -95,12 +92,9 @@ public final class StorageSyncModels {
|
|||||||
|
|
||||||
public static ContactRecord localToRemoteRecord(Recipient recipient, IdentityInfo identity) {
|
public static ContactRecord localToRemoteRecord(Recipient recipient, IdentityInfo identity) {
|
||||||
final var address = recipient.getAddress();
|
final var address = recipient.getAddress();
|
||||||
final var aciPresent = address.aci().isPresent();
|
|
||||||
|
|
||||||
final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
|
final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
|
||||||
.e164(address.number().orElse(""))
|
.e164(address.number().orElse(""))
|
||||||
.username(address.username().orElse(""))
|
.username(address.username().orElse(""))
|
||||||
.pniSignatureVerified(recipient.isPniSignatureVerified())
|
|
||||||
.profileKey(recipient.getProfileKey() == null
|
.profileKey(recipient.getProfileKey() == null
|
||||||
? ByteString.EMPTY
|
? ByteString.EMPTY
|
||||||
: ByteString.of(recipient.getProfileKey().serialize()));
|
: ByteString.of(recipient.getProfileKey().serialize()));
|
||||||
@ -123,7 +117,6 @@ public final class StorageSyncModels {
|
|||||||
.nickname(getNicknameRemoteRecord(recipient.getContact()))
|
.nickname(getNicknameRemoteRecord(recipient.getContact()))
|
||||||
.note(emptyIfNull(recipient.getContact().note()))
|
.note(emptyIfNull(recipient.getContact().note()))
|
||||||
.blocked(recipient.getContact().isBlocked())
|
.blocked(recipient.getContact().isBlocked())
|
||||||
.blockedAtTimestamp(recipient.getContact().blockedAt())
|
|
||||||
.whitelisted(recipient.getContact().isProfileSharingEnabled())
|
.whitelisted(recipient.getContact().isProfileSharingEnabled())
|
||||||
.mutedUntilTimestamp(recipient.getContact().muteUntil())
|
.mutedUntilTimestamp(recipient.getContact().muteUntil())
|
||||||
.hideStory(recipient.getContact().hideStory())
|
.hideStory(recipient.getContact().hideStory())
|
||||||
@ -133,7 +126,7 @@ public final class StorageSyncModels {
|
|||||||
.archived(recipient.getContact().isArchived())
|
.archived(recipient.getContact().isArchived())
|
||||||
.hidden(recipient.getContact().isHidden());
|
.hidden(recipient.getContact().isHidden());
|
||||||
}
|
}
|
||||||
if (identity != null && aciPresent) {
|
if (identity != null) {
|
||||||
builder.identityKey(ByteString.of(identity.getIdentityKey().serialize()))
|
builder.identityKey(ByteString.of(identity.getIdentityKey().serialize()))
|
||||||
.identityState(localToRemote(identity.getTrustLevel()));
|
.identityState(localToRemote(identity.getTrustLevel()));
|
||||||
}
|
}
|
||||||
@ -162,28 +155,10 @@ public final class StorageSyncModels {
|
|||||||
final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
|
final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
|
||||||
builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
|
builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
|
||||||
builder.blocked(group.isBlocked());
|
builder.blocked(group.isBlocked());
|
||||||
builder.blockedAtTimestamp(group.getBlockedAt());
|
|
||||||
builder.whitelisted(group.isProfileSharingEnabled());
|
builder.whitelisted(group.isProfileSharingEnabled());
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StickerPackRecord localToRemoteRecord(StickerPack stickerPack) {
|
|
||||||
final var builder = SignalStickerPackRecord.Companion.newBuilder(stickerPack.storageRecord());
|
|
||||||
builder.packId(ByteString.of(stickerPack.packId().serialize()));
|
|
||||||
|
|
||||||
if (stickerPack.deletedTimestamp() > 0) {
|
|
||||||
builder.packKey(ByteString.EMPTY);
|
|
||||||
builder.position(0);
|
|
||||||
builder.deletedAtTimestamp(stickerPack.deletedTimestamp());
|
|
||||||
} else {
|
|
||||||
builder.packKey(ByteString.of(stickerPack.packKey()));
|
|
||||||
builder.position(stickerPack.position());
|
|
||||||
builder.deletedAtTimestamp(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TrustLevel remoteToLocal(IdentityState identityState) {
|
public static TrustLevel remoteToLocal(IdentityState identityState) {
|
||||||
return switch (identityState) {
|
return switch (identityState) {
|
||||||
case DEFAULT -> TrustLevel.TRUSTED_UNVERIFIED;
|
case DEFAULT -> TrustLevel.TRUSTED_UNVERIFIED;
|
||||||
|
|||||||
@ -168,11 +168,6 @@ public final class StorageSyncValidations {
|
|||||||
throw new DuplicateCallLinkError();
|
throw new DuplicateCallLinkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.STICKER_PACK.getValue());
|
|
||||||
if (ids.size() != new HashSet<>(ids).size()) {
|
|
||||||
throw new DuplicateStickerPackError();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DuplicateRawIdAcrossTypesError();
|
throw new DuplicateRawIdAcrossTypesError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,8 +217,6 @@ public final class StorageSyncValidations {
|
|||||||
|
|
||||||
private static final class DuplicateInsertInWriteError extends Error {}
|
private static final class DuplicateInsertInWriteError extends Error {}
|
||||||
|
|
||||||
private static final class DuplicateStickerPackError extends Error {}
|
|
||||||
|
|
||||||
private static final class InsertNotPresentInFullIdSetError extends Error {}
|
private static final class InsertNotPresentInFullIdSetError extends Error {}
|
||||||
|
|
||||||
private static final class DeletePresentInFullIdSetError extends Error {}
|
private static final class DeletePresentInFullIdSetError extends Error {}
|
||||||
|
|||||||
@ -1,44 +1,51 @@
|
|||||||
package org.asamk.signal.manager.util;
|
package org.asamk.signal.manager.util;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
|
|
||||||
public class AttachmentUtils {
|
public class AttachmentUtils {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AttachmentUtils.class);
|
public static SignalServiceAttachmentStream createAttachmentStream(
|
||||||
|
String attachment,
|
||||||
|
boolean voiceNote,
|
||||||
|
ResumableUploadSpec resumableUploadSpec
|
||||||
|
) throws AttachmentInvalidException {
|
||||||
|
try {
|
||||||
|
final var streamDetails = Utils.createStreamDetails(attachment);
|
||||||
|
|
||||||
// Images are fully buffered in memory to probe their dimensions, so cap how large a file we'll do this for.
|
return createAttachmentStream(streamDetails.first(), streamDetails.second(), voiceNote, resumableUploadSpec);
|
||||||
private static final long MAX_DIMENSION_PROBE_SIZE = 20 * 1024 * 1024;
|
} catch (IOException e) {
|
||||||
|
throw new AttachmentInvalidException(attachment, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceAttachmentStream createAttachmentStream(
|
||||||
|
String attachment,
|
||||||
|
ResumableUploadSpec resumableUploadSpec
|
||||||
|
) throws AttachmentInvalidException {
|
||||||
|
return createAttachmentStream(attachment, false, resumableUploadSpec);
|
||||||
|
}
|
||||||
|
|
||||||
public static SignalServiceAttachmentStream createAttachmentStream(
|
public static SignalServiceAttachmentStream createAttachmentStream(
|
||||||
StreamDetails streamDetails,
|
StreamDetails streamDetails,
|
||||||
Optional<String> name,
|
Optional<String> name,
|
||||||
boolean voiceNote,
|
boolean voiceNote,
|
||||||
ResumableUploadSpec resumableUploadSpec
|
ResumableUploadSpec resumableUploadSpec
|
||||||
) throws ResumeLocationInvalidException, IOException {
|
) throws ResumeLocationInvalidException {
|
||||||
final var uploadTimestamp = System.currentTimeMillis();
|
final var uploadTimestamp = System.currentTimeMillis();
|
||||||
final var probedStream = probeImageDimensions(streamDetails);
|
|
||||||
return SignalServiceAttachmentStream.newStreamBuilder()
|
return SignalServiceAttachmentStream.newStreamBuilder()
|
||||||
.withStream(probedStream.inputStream())
|
.withStream(streamDetails.getStream())
|
||||||
.withContentType(streamDetails.getContentType())
|
.withContentType(streamDetails.getContentType())
|
||||||
.withLength(streamDetails.getLength())
|
.withLength(streamDetails.getLength())
|
||||||
.withFileName(name.orElse(null))
|
.withFileName(name.orElse(null))
|
||||||
.withVoiceNote(voiceNote)
|
.withVoiceNote(voiceNote)
|
||||||
.withWidth(probedStream.width())
|
|
||||||
.withHeight(probedStream.height())
|
|
||||||
.withUploadTimestamp(uploadTimestamp)
|
.withUploadTimestamp(uploadTimestamp)
|
||||||
.withResumableUploadSpec(resumableUploadSpec)
|
.withResumableUploadSpec(resumableUploadSpec)
|
||||||
.withUuid(UUID.randomUUID())
|
.withUuid(UUID.randomUUID())
|
||||||
@ -49,55 +56,7 @@ public class AttachmentUtils {
|
|||||||
StreamDetails streamDetails,
|
StreamDetails streamDetails,
|
||||||
Optional<String> name,
|
Optional<String> name,
|
||||||
ResumableUploadSpec resumableUploadSpec
|
ResumableUploadSpec resumableUploadSpec
|
||||||
) throws ResumeLocationInvalidException, IOException {
|
) throws ResumeLocationInvalidException {
|
||||||
return createAttachmentStream(streamDetails, name, false, resumableUploadSpec);
|
return createAttachmentStream(streamDetails, name, false, resumableUploadSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the attachment's dimensions if it's an image, so recipients don't get a square-cropped thumbnail.
|
|
||||||
* Falls back to width/height 0 (today's behavior) if the content isn't an image, is too large to probe
|
|
||||||
* cheaply, or fails to parse.
|
|
||||||
*/
|
|
||||||
private static ProbedStream probeImageDimensions(StreamDetails streamDetails) throws IOException {
|
|
||||||
final var contentType = streamDetails.getContentType();
|
|
||||||
final var length = streamDetails.getLength();
|
|
||||||
if (contentType == null
|
|
||||||
|| !contentType.startsWith("image/")
|
|
||||||
|| length <= 0
|
|
||||||
|| length > MAX_DIMENSION_PROBE_SIZE) {
|
|
||||||
return new ProbedStream(streamDetails.getStream(), 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
final var stream = streamDetails.getStream();
|
|
||||||
var width = 0;
|
|
||||||
var height = 0;
|
|
||||||
if (stream instanceof FileInputStream fis) {
|
|
||||||
try {
|
|
||||||
final var image = ImageIO.read(fis);
|
|
||||||
if (image != null) {
|
|
||||||
width = image.getWidth();
|
|
||||||
height = image.getHeight();
|
|
||||||
}
|
|
||||||
fis.getChannel().position(0);
|
|
||||||
} catch (IOException | LinkageError e) {
|
|
||||||
logger.debug("Failed to probe image dimensions, sending without width/height: {}", e.getMessage());
|
|
||||||
fis.getChannel().position(0);
|
|
||||||
}
|
|
||||||
return new ProbedStream(fis, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
final var bytes = stream.readAllBytes();
|
|
||||||
try {
|
|
||||||
final var image = ImageIO.read(new ByteArrayInputStream(bytes));
|
|
||||||
if (image != null) {
|
|
||||||
width = image.getWidth();
|
|
||||||
height = image.getHeight();
|
|
||||||
}
|
|
||||||
} catch (IOException | LinkageError e) {
|
|
||||||
logger.debug("Failed to probe image dimensions, sending without width/height: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
return new ProbedStream(new ByteArrayInputStream(bytes), width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private record ProbedStream(InputStream inputStream, int width, int height) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,19 +21,9 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
|
|||||||
public class IOUtils {
|
public class IOUtils {
|
||||||
|
|
||||||
public static File createTempFile() throws IOException {
|
public static File createTempFile() throws IOException {
|
||||||
final var prefix = "signal-cli_tmp_";
|
final var tempFile = File.createTempFile("signal-cli_tmp_", ".tmp");
|
||||||
final var suffix = ".tmp";
|
tempFile.deleteOnExit();
|
||||||
try {
|
return tempFile;
|
||||||
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
|
|
||||||
var path = Files.createTempFile(prefix, suffix, PosixFilePermissions.asFileAttribute(perms));
|
|
||||||
var tempFile = path.toFile();
|
|
||||||
tempFile.deleteOnExit();
|
|
||||||
return tempFile;
|
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
final var tempFile = File.createTempFile(prefix, suffix);
|
|
||||||
tempFile.deleteOnExit();
|
|
||||||
return tempFile;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] readFully(InputStream in) throws IOException {
|
public static byte[] readFully(InputStream in) throws IOException {
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
package org.asamk.signal.manager.util;
|
|
||||||
|
|
||||||
public final class LeakyBucket {
|
|
||||||
|
|
||||||
private final int capacity;
|
|
||||||
private final long dripIntervalMillis;
|
|
||||||
private final State state;
|
|
||||||
|
|
||||||
public LeakyBucket(final int capacity, final long dripIntervalMillis, final State state) {
|
|
||||||
this.capacity = capacity;
|
|
||||||
this.dripIntervalMillis = dripIntervalMillis;
|
|
||||||
this.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int level(final long now) {
|
|
||||||
return calculateStateForCurrentTime(now).level();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasRoom(final long now) {
|
|
||||||
return level(now) < capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void use(final long now) {
|
|
||||||
final var currentState = calculateStateForCurrentTime(now);
|
|
||||||
state.update(currentState.level() + 1, currentState.levelUpdatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refund(final long now) {
|
|
||||||
final var currentState = calculateStateForCurrentTime(now);
|
|
||||||
state.update(Math.max(currentState.level() - 1, 0), currentState.levelUpdatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
state.update(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Snapshot calculateStateForCurrentTime(final long now) {
|
|
||||||
final var level = state.level();
|
|
||||||
final var levelUpdatedAt = state.levelUpdatedAt();
|
|
||||||
final var elapsed = now - levelUpdatedAt;
|
|
||||||
|
|
||||||
if (level <= 0 || elapsed < 0) {
|
|
||||||
return new Snapshot(0, now);
|
|
||||||
}
|
|
||||||
|
|
||||||
final var drips = elapsed / dripIntervalMillis;
|
|
||||||
return new Snapshot((int) Math.max(level - drips, 0), levelUpdatedAt + dripIntervalMillis * drips);
|
|
||||||
}
|
|
||||||
|
|
||||||
private record Snapshot(int level, long levelUpdatedAt) {}
|
|
||||||
|
|
||||||
public interface State {
|
|
||||||
|
|
||||||
int level();
|
|
||||||
|
|
||||||
long levelUpdatedAt();
|
|
||||||
|
|
||||||
void update(int level, long levelUpdatedAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -10,11 +10,11 @@ import org.asamk.signal.manager.api.RateLimitException;
|
|||||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||||
import org.asamk.signal.manager.helper.PinHelper;
|
import org.asamk.signal.manager.helper.PinHelper;
|
||||||
import org.signal.core.models.MasterKey;
|
import org.signal.core.models.MasterKey;
|
||||||
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.push.exceptions.ChallengeRequiredException;
|
import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException;
|
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException;
|
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException;
|
||||||
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
||||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||||
@ -65,12 +65,14 @@ public class NumberVerificationUtils {
|
|||||||
if (nextAttempt == null) {
|
if (nextAttempt == null) {
|
||||||
throw new VerificationMethodNotAvailableException();
|
throw new VerificationMethodNotAvailableException();
|
||||||
} else if (nextAttempt > 0) {
|
} else if (nextAttempt > 0) {
|
||||||
throw new RateLimitException(nextAttempt * 1000L);
|
final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextAttempt * 1000;
|
||||||
|
throw new RateLimitException(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
final var nextVerificationAttempt = sessionResponse.getMetadata().getNextVerificationAttempt();
|
final var nextVerificationAttempt = sessionResponse.getMetadata().getNextVerificationAttempt();
|
||||||
if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
|
if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
|
||||||
throw new CaptchaRequiredException(nextVerificationAttempt * 1000L);
|
final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextVerificationAttempt * 1000;
|
||||||
|
throw new CaptchaRequiredException(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionResponse.getMetadata().getRequestedInformation().contains("captcha")) {
|
if (sessionResponse.getMetadata().getRequestedInformation().contains("captcha")) {
|
||||||
|
|||||||
@ -78,10 +78,8 @@ public class StickerUtils {
|
|||||||
throw new StickerPackInvalidException("Could not find find " + pack.cover().file());
|
throw new StickerPackInvalidException("Could not find find " + pack.cover().file());
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentType = pack.cover().contentType() != null && !pack.cover().contentType().isEmpty()
|
var contentType = pack.cover().contentType() != null && !pack.cover().contentType().isEmpty() ? pack.cover()
|
||||||
? pack.cover()
|
.contentType() : getContentType(rootPath, zip, pack.cover().file());
|
||||||
.contentType()
|
|
||||||
: getContentType(rootPath, zip, pack.cover().file());
|
|
||||||
cover = new SignalServiceStickerManifestUpload.StickerInfo(data.first(),
|
cover = new SignalServiceStickerManifestUpload.StickerInfo(data.first(),
|
||||||
data.second(),
|
data.second(),
|
||||||
Optional.ofNullable(pack.cover().emoji()).orElse(""),
|
Optional.ofNullable(pack.cover().emoji()).orElse(""),
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import org.signal.libsignal.net.RequestResult;
|
|||||||
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.NumericFingerprintGenerator;
|
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
|
||||||
import org.signal.network.NetworkResult;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.NetworkResult;
|
||||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.Proxy;
|
import java.net.Proxy;
|
||||||
import java.net.ProxySelector;
|
import java.net.ProxySelector;
|
||||||
@ -38,9 +39,6 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
import kotlin.coroutines.Continuation;
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext;
|
|
||||||
import kotlinx.coroutines.BuildersKt;
|
|
||||||
import okio.ByteString;
|
import okio.ByteString;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
@ -55,7 +53,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static StreamDetails createStreamDetailsFromFile(final File file) throws IOException {
|
public static StreamDetails createStreamDetailsFromFile(final File file) throws IOException {
|
||||||
final var stream = new FileInputStream(file);
|
final InputStream stream = new FileInputStream(file);
|
||||||
final var size = file.length();
|
final var size = file.length();
|
||||||
final var mime = MimeUtils.getFileMimeType(file).orElse(MimeUtils.OCTET_STREAM);
|
final var mime = MimeUtils.getFileMimeType(file).orElse(MimeUtils.OCTET_STREAM);
|
||||||
return new StreamDetails(stream, mime, size);
|
return new StreamDetails(stream, mime, size);
|
||||||
@ -163,24 +161,6 @@ public class Utils {
|
|||||||
return NetworkResultUtil.toBasicLegacy(response);
|
return NetworkResultUtil.toBasicLegacy(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T> T runSuspendBlocking(final Function<Continuation<? super T>, Object> call) {
|
|
||||||
try {
|
|
||||||
return (T) BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
|
||||||
(scope, cont) -> call.apply((Continuation<? super T>) cont));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new RuntimeException("Interrupted while waiting for suspend function", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T, E extends BadRequestError> T handleResponseExceptionSuspend(
|
|
||||||
final Function<Continuation<? super RequestResult>, Object> call
|
|
||||||
) throws IOException {
|
|
||||||
return handleResponseException((RequestResult<T, E>) runSuspendBlocking((Function) call));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static <T, E extends BadRequestError> T handleResponseException(final RequestResult<T, E> result) throws IOException {
|
public static <T, E extends BadRequestError> T handleResponseException(final RequestResult<T, E> result) throws IOException {
|
||||||
if (result instanceof RequestResult.Success<?> success) {
|
if (result instanceof RequestResult.Success<?> success) {
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
package org.asamk.signal.manager.storage.groups;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.Group;
|
|
||||||
import org.asamk.signal.manager.api.GroupId;
|
|
||||||
import org.asamk.signal.manager.groups.GroupUtils;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.TestRecipientId;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.signal.core.models.ServiceId;
|
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
|
||||||
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class GroupInfoTerminatedTest {
|
|
||||||
|
|
||||||
private static final RecipientResolver UNUSED_RESOLVER = new RecipientResolver() {
|
|
||||||
@Override
|
|
||||||
public RecipientId resolveRecipient(final RecipientAddress address) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecipientId resolveRecipient(final long recipientId) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecipientId resolveRecipient(final String identifier) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecipientId resolveRecipient(final ServiceId serviceId) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static GroupMasterKey masterKey() {
|
|
||||||
final var bytes = new byte[32];
|
|
||||||
for (int i = 0; i < bytes.length; i++) {
|
|
||||||
bytes[i] = (byte) (i + 1);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return new GroupMasterKey(bytes);
|
|
||||||
} catch (InvalidInputException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GroupInfoV2 groupV2(final DecryptedGroup group) {
|
|
||||||
final var masterKey = masterKey();
|
|
||||||
return new GroupInfoV2(GroupUtils.getGroupIdV2(masterKey),
|
|
||||||
masterKey,
|
|
||||||
group,
|
|
||||||
DistributionId.create(),
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
UNUSED_RESOLVER);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void v1GroupsAreNeverTerminated() {
|
|
||||||
final var group = new GroupInfoV1(GroupId.v1(new byte[16]));
|
|
||||||
assertFalse(group.isTerminated());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void v2ReadsTerminatedFlagFromDecryptedGroup() {
|
|
||||||
assertTrue(groupV2(new DecryptedGroup.Builder().terminated(true).build()).isTerminated());
|
|
||||||
assertFalse(groupV2(new DecryptedGroup.Builder().terminated(false).build()).isTerminated());
|
|
||||||
assertFalse(groupV2(new DecryptedGroup.Builder().build()).isTerminated());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void v2IsNotTerminatedWhenGroupStateMissing() {
|
|
||||||
final var masterKey = masterKey();
|
|
||||||
final var group = new GroupInfoV2(GroupUtils.getGroupIdV2(masterKey), masterKey, UNUSED_RESOLVER);
|
|
||||||
assertFalse(group.isTerminated());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void groupApiRecordCarriesTerminatedFromModel() {
|
|
||||||
final RecipientId self = TestRecipientId.createTestId(1);
|
|
||||||
|
|
||||||
final var terminated = Group.from(groupV2(new DecryptedGroup.Builder().terminated(true).build()),
|
|
||||||
recipientId -> null,
|
|
||||||
self);
|
|
||||||
assertTrue(terminated.isTerminated());
|
|
||||||
|
|
||||||
final var live = Group.from(groupV2(new DecryptedGroup.Builder().terminated(false).build()),
|
|
||||||
recipientId -> null,
|
|
||||||
self);
|
|
||||||
assertFalse(live.isTerminated());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
package org.asamk.signal.manager.storage.recipients;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.Contact;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class RecipientStoreTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mergeContactsUsesAndroidMergePolicy() {
|
|
||||||
final var primary = Contact.newBuilder()
|
|
||||||
.withGivenName("Primary given")
|
|
||||||
.withFamilyName("Primary family")
|
|
||||||
.withNickName("Primary system nickname")
|
|
||||||
.withNickNameGivenName("Primary nickname given")
|
|
||||||
.withNickNameFamilyName("Primary nickname family")
|
|
||||||
.withNote("Primary note")
|
|
||||||
.withColor("Primary color")
|
|
||||||
.withMessageExpirationTimeVersion(2)
|
|
||||||
.withHideStory(true)
|
|
||||||
.withIsBlocked(false)
|
|
||||||
.withBlockedAt(100)
|
|
||||||
.withIsArchived(true)
|
|
||||||
.withIsHidden(true)
|
|
||||||
.withUnregisteredTimestamp(300L)
|
|
||||||
.build();
|
|
||||||
final var secondary = Contact.newBuilder()
|
|
||||||
.withGivenName("Secondary given")
|
|
||||||
.withFamilyName("Secondary family")
|
|
||||||
.withNickName("Secondary system nickname")
|
|
||||||
.withNickNameGivenName("Secondary nickname given")
|
|
||||||
.withNickNameFamilyName("Secondary nickname family")
|
|
||||||
.withNote("Secondary note")
|
|
||||||
.withColor("Secondary color")
|
|
||||||
.withMessageExpirationTime(60)
|
|
||||||
.withMessageExpirationTimeVersion(3)
|
|
||||||
.withMuteUntil(400)
|
|
||||||
.withIsBlocked(true)
|
|
||||||
.withBlockedAt(200)
|
|
||||||
.withIsProfileSharingEnabled(true)
|
|
||||||
.withIsHidden(true)
|
|
||||||
.withUnregisteredTimestamp(500L)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final var merged = RecipientStore.mergeContacts(primary, secondary);
|
|
||||||
|
|
||||||
assertEquals("Secondary given", merged.givenName());
|
|
||||||
assertEquals("Secondary family", merged.familyName());
|
|
||||||
assertEquals("Primary system nickname", merged.nickName());
|
|
||||||
assertEquals("Primary nickname given", merged.nickNameGivenName());
|
|
||||||
assertEquals("Primary nickname family", merged.nickNameFamilyName());
|
|
||||||
assertEquals("Primary note", merged.note());
|
|
||||||
assertEquals("Primary color", merged.color());
|
|
||||||
assertEquals(60, merged.messageExpirationTime());
|
|
||||||
assertEquals(3, merged.messageExpirationTimeVersion());
|
|
||||||
assertEquals(400, merged.muteUntil());
|
|
||||||
assertTrue(merged.hideStory());
|
|
||||||
assertTrue(merged.isBlocked());
|
|
||||||
assertEquals(200, merged.blockedAt());
|
|
||||||
assertTrue(merged.isArchived());
|
|
||||||
assertTrue(merged.isProfileSharingEnabled());
|
|
||||||
assertFalse(merged.isHidden());
|
|
||||||
assertEquals(300L, merged.unregisteredTimestamp());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mergeContactsPrefersConfiguredPrimaryValues() {
|
|
||||||
final var primary = Contact.newBuilder()
|
|
||||||
.withColor("Primary color")
|
|
||||||
.withMessageExpirationTime(30)
|
|
||||||
.withMessageExpirationTimeVersion(4)
|
|
||||||
.withMuteUntil(100)
|
|
||||||
.withIsHidden(true)
|
|
||||||
.build();
|
|
||||||
final var secondary = Contact.newBuilder()
|
|
||||||
.withColor("Secondary color")
|
|
||||||
.withMessageExpirationTime(60)
|
|
||||||
.withMessageExpirationTimeVersion(3)
|
|
||||||
.withMuteUntil(200)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final var merged = RecipientStore.mergeContacts(primary, secondary);
|
|
||||||
|
|
||||||
assertEquals("Primary color", merged.color());
|
|
||||||
assertEquals(30, merged.messageExpirationTime());
|
|
||||||
assertEquals(4, merged.messageExpirationTimeVersion());
|
|
||||||
assertEquals(100, merged.muteUntil());
|
|
||||||
assertTrue(merged.isHidden());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package org.asamk.signal.manager.syncStorage;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class StorageRecordProcessorTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void splitsOnlyUnregisteredAciOnlyRecords() {
|
|
||||||
final var aci = ACI.from(UUID.randomUUID());
|
|
||||||
final var pni = PNI.from(UUID.randomUUID());
|
|
||||||
|
|
||||||
assertTrue(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, null, ""));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(0, aci, null, ""));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, null, null, ""));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, pni, ""));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, null, "+12025550123"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void keepsOlderLocalStickerDeletion() {
|
|
||||||
assertTrue(StickerPackRecordProcessor.shouldKeepLocalDeletion(200, 100));
|
|
||||||
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(100, 200));
|
|
||||||
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(200, 0));
|
|
||||||
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(0, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
package org.asamk.signal.manager.syncStorage;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
||||||
|
|
||||||
class StorageSyncLoopDetectorTest {
|
|
||||||
|
|
||||||
private static final long NOW = 1_700_000_000_000L;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void repeatedPayloadIsDeniedAfterThreeCharges() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var write = writeWithInsert(1);
|
|
||||||
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
for (var index = 0; index < 3; index++) {
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.REPEATED_PAYLOAD, 3),
|
|
||||||
detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void storageIdsAreExcludedFromPayloadFingerprint() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
|
|
||||||
assertAllowed(detector.onWriteAttempt(writeWithInsert(1), true, false, NOW));
|
|
||||||
assertAllowed(detector.onWriteAttempt(writeWithInsert(2), true, false, NOW));
|
|
||||||
assertAllowed(detector.onWriteAttempt(writeWithInsert(3), true, false, NOW));
|
|
||||||
assertAllowed(detector.onWriteAttempt(writeWithInsert(4), true, false, NOW));
|
|
||||||
|
|
||||||
assertInstanceOf(StorageSyncLoopDetector.Decision.Denied.class,
|
|
||||||
detector.onWriteAttempt(writeWithInsert(5), true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void convergenceClearsTheContentBucket() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var write = writeWithInsert(1);
|
|
||||||
for (var index = 0; index < 4; index++) {
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
detector.onConverged();
|
|
||||||
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void failedWritesAreRefunded() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var write = writeWithInsert(1);
|
|
||||||
|
|
||||||
for (var index = 0; index < 20; index++) {
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
detector.onWriteFailed(NOW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void rateBucketLimitsDeleteOnlyWrites() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var write = new WriteOperationResult(null, List.of(), List.of(new byte[]{1}));
|
|
||||||
|
|
||||||
for (var index = 0; index < 100; index++) {
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.WRITE_RATE, 100),
|
|
||||||
detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void retriesAndSingleDeviceWritesAreExempt() {
|
|
||||||
final var retryDetector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var singleDeviceDetector = new StorageSyncLoopDetector(() -> false);
|
|
||||||
final var write = writeWithInsert(1);
|
|
||||||
|
|
||||||
for (var index = 0; index < 20; index++) {
|
|
||||||
assertAllowed(retryDetector.onWriteAttempt(write, true, true, NOW));
|
|
||||||
assertAllowed(singleDeviceDetector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static WriteOperationResult writeWithInsert(final int storageIdByte) {
|
|
||||||
final var storageId = StorageId.forType(new byte[]{(byte) storageIdByte}, 99);
|
|
||||||
final var record = new SignalStorageRecord(storageId, new StorageRecord.Builder().build());
|
|
||||||
return new WriteOperationResult(null, List.of(record), List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertAllowed(final StorageSyncLoopDetector.Decision decision) {
|
|
||||||
assertEquals(StorageSyncLoopDetector.Decision.Allowed.INSTANCE, decision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
package org.asamk.signal.manager.util;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
class AttachmentUtilsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createAttachmentStream_setsWidthAndHeightForImage() throws Exception {
|
|
||||||
final var imageBytes = pngBytes(37, 21);
|
|
||||||
final var streamDetails = new StreamDetails(new ByteArrayInputStream(imageBytes),
|
|
||||||
"image/png",
|
|
||||||
imageBytes.length);
|
|
||||||
|
|
||||||
final var attachment = AttachmentUtils.createAttachmentStream(streamDetails, Optional.of("meme.png"), null);
|
|
||||||
|
|
||||||
assertEquals(37, attachment.getWidth());
|
|
||||||
assertEquals(21, attachment.getHeight());
|
|
||||||
assertArrayEquals(imageBytes, attachment.getInputStream().readAllBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createAttachmentStream_leavesWidthAndHeightZeroForNonImage() throws Exception {
|
|
||||||
final var bytes = "not an image".getBytes();
|
|
||||||
final var streamDetails = new StreamDetails(new ByteArrayInputStream(bytes),
|
|
||||||
"application/octet-stream",
|
|
||||||
bytes.length);
|
|
||||||
|
|
||||||
final var attachment = AttachmentUtils.createAttachmentStream(streamDetails, Optional.of("file.bin"), null);
|
|
||||||
|
|
||||||
assertEquals(0, attachment.getWidth());
|
|
||||||
assertEquals(0, attachment.getHeight());
|
|
||||||
assertArrayEquals(bytes, attachment.getInputStream().readAllBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] pngBytes(final int width, final int height) throws Exception {
|
|
||||||
final var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
||||||
final var out = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(image, "png", out);
|
|
||||||
return out.toByteArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
package org.asamk.signal.manager.util;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class LeakyBucketTest {
|
|
||||||
|
|
||||||
private static final long NOW = 1_700_000_000_000L;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void dripsLevelsAndCarriesPartialIntervals() {
|
|
||||||
final var state = new TestState();
|
|
||||||
final var bucket = new LeakyBucket(3, Duration.ofHours(1).toMillis(), state);
|
|
||||||
|
|
||||||
bucket.use(NOW);
|
|
||||||
bucket.use(NOW);
|
|
||||||
bucket.use(NOW);
|
|
||||||
|
|
||||||
assertEquals(2, bucket.level(NOW + Duration.ofMinutes(90).toMillis()));
|
|
||||||
assertEquals(1, bucket.level(NOW + Duration.ofMinutes(121).toMillis()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void refundNeverDropsBelowZero() {
|
|
||||||
final var bucket = new LeakyBucket(1, 1_000, new TestState());
|
|
||||||
|
|
||||||
bucket.refund(NOW);
|
|
||||||
|
|
||||||
assertEquals(0, bucket.level(NOW));
|
|
||||||
assertTrue(bucket.hasRoom(NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void clockMovingBackwardsRefillsTheBucket() {
|
|
||||||
final var bucket = new LeakyBucket(1, 1_000, new TestState());
|
|
||||||
bucket.use(NOW);
|
|
||||||
|
|
||||||
assertTrue(bucket.hasRoom(NOW - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class TestState implements LeakyBucket.State {
|
|
||||||
|
|
||||||
private int level;
|
|
||||||
private long levelUpdatedAt;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int level() {
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long levelUpdatedAt() {
|
|
||||||
return levelUpdatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void update(final int level, final long levelUpdatedAt) {
|
|
||||||
this.level = level;
|
|
||||||
this.levelUpdatedAt = levelUpdatedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +1 @@
|
|||||||
0.99.1
|
0.90.0
|
||||||
|
|||||||
@ -14,8 +14,8 @@ all: $(MANPAGESRC)
|
|||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: all
|
install: all
|
||||||
$(MKDIR) -p man1 man5
|
$(MKDIR) -p man1 man5
|
||||||
for f in *.1; do $(GZIP) -n < "$$f" > man1/"$$f".gz ; done
|
for f in *.1; do $(GZIP) < "$$f" > man1/"$$f".gz ; done
|
||||||
for f in *.5; do $(GZIP) -n < "$$f" > man5/"$$f".gz ; done
|
for f in *.5; do $(GZIP) < "$$f" > man5/"$$f".gz ; done
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
@ -13,7 +13,7 @@ signal-cli-dbus - A commandline and dbus interface for the Signal messenger
|
|||||||
|
|
||||||
== Synopsis
|
== Synopsis
|
||||||
|
|
||||||
*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system]
|
*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system]
|
||||||
|
|
||||||
*dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/_<phonenumber>] org.asamk.Signal.<method> [string:<string argument>] [array:<type>:<array argument>]
|
*dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/_<phonenumber>] org.asamk.Signal.<method> [string:<string argument>] [array:<type>:<array argument>]
|
||||||
|
|
||||||
@ -452,7 +452,6 @@ Groups have the following (case-sensitive) properties:
|
|||||||
* IsBlocked<b> : true=member will not receive group messages; false=not blocked
|
* IsBlocked<b> : true=member will not receive group messages; false=not blocked
|
||||||
* IsMember<b> (read-only) : always true (object path exists only for group members)
|
* IsMember<b> (read-only) : always true (object path exists only for group members)
|
||||||
* IsAdmin<b> (read-only) : true=member has admin privileges; false=not admin
|
* IsAdmin<b> (read-only) : true=member has admin privileges; false=not admin
|
||||||
* IsTerminated<b> (read-only) : true=group was permanently ended by an admin; nobody (not even an admin) can send messages or start calls, sending is rejected locally
|
|
||||||
* MessageExpirationTimer<i> : int32 representing message expiration time for group
|
* MessageExpirationTimer<i> : int32 representing message expiration time for group
|
||||||
* Members<as> (read-only) : String array of group members' phone numbers
|
* Members<as> (read-only) : String array of group members' phone numbers
|
||||||
* PendingMembers<as> (read-only) : String array of pending members' phone numbers
|
* PendingMembers<as> (read-only) : String array of pending members' phone numbers
|
||||||
@ -503,11 +502,6 @@ Exceptions: Failure
|
|||||||
quitGroup() -> <>::
|
quitGroup() -> <>::
|
||||||
Exceptions: Failure, LastGroupAdmin
|
Exceptions: Failure, LastGroupAdmin
|
||||||
|
|
||||||
terminateGroup() -> <>::
|
|
||||||
Permanently terminate the group for all members (requires admin privileges). Afterwards nobody can send messages or start calls; members keep access to the existing history.
|
|
||||||
|
|
||||||
Exceptions: Failure
|
|
||||||
|
|
||||||
removeAdmins(recipients<as>) -> <>::
|
removeAdmins(recipients<as>) -> <>::
|
||||||
* recipients : String array of phone numbers
|
* recipients : String array of phone numbers
|
||||||
|
|
||||||
|
|||||||
@ -13,9 +13,9 @@ signal-cli-jsonrpc - A commandline and dbus interface for the Signal messenger
|
|||||||
|
|
||||||
== Synopsis
|
== Synopsis
|
||||||
|
|
||||||
*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]]
|
*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]]
|
||||||
|
|
||||||
*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] jsonRpc
|
*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] jsonRpc
|
||||||
|
|
||||||
== Description
|
== Description
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ signal-cli - A commandline interface for the Signal messenger
|
|||||||
|
|
||||||
== Synopsis
|
== Synopsis
|
||||||
|
|
||||||
*signal-cli* [--data-dir DATA_DIR] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options]
|
*signal-cli* [--config CONFIG] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options]
|
||||||
|
|
||||||
== Description
|
== Description
|
||||||
|
|
||||||
@ -57,8 +57,8 @@ If `--verbose` is also given, the detailed logs will only be written to the log
|
|||||||
Scrub possibly sensitive information from the log, like phone numbers and UUIDs.
|
Scrub possibly sensitive information from the log, like phone numbers and UUIDs.
|
||||||
Doesn't work reliably on dbus logs with very verbose logging (`-vvv`)
|
Doesn't work reliably on dbus logs with very verbose logging (`-vvv`)
|
||||||
|
|
||||||
*-d* DATA_DIR, *--data-dir* DATA_DIR, *-c* CONFIG, *--config* CONFIG::
|
*--config* CONFIG::
|
||||||
Set the path where to store account data and local configuration.
|
Set the path, where to store the config.
|
||||||
Make sure you have full read/write access to the given directory.
|
Make sure you have full read/write access to the given directory.
|
||||||
(Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`))
|
(Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`))
|
||||||
|
|
||||||
@ -632,20 +632,6 @@ Specify the timestamp of the message to which to react.
|
|||||||
*--type* TYPE::
|
*--type* TYPE::
|
||||||
Specify the receipt type, either `read` (the default) or `viewed`.
|
Specify the receipt type, either `read` (the default) or `viewed`.
|
||||||
|
|
||||||
=== sendStory
|
|
||||||
|
|
||||||
Post a file attachment story to your Story, visible to all contacts.
|
|
||||||
|
|
||||||
*-a* ATTACHMENT, *--attachment* ATTACHMENT::
|
|
||||||
Specify the file path to the image or video to post as a story.
|
|
||||||
|
|
||||||
*-g* GROUP, *--group-id* GROUP::
|
|
||||||
Specify a group to post the story to.
|
|
||||||
Without this flag, the story is posted to "My Story".
|
|
||||||
|
|
||||||
*--no-replies*::
|
|
||||||
Disable replies on this story. By default, replies are allowed.
|
|
||||||
|
|
||||||
=== sendTyping
|
=== sendTyping
|
||||||
|
|
||||||
Send typing message to trigger a typing indicator for the recipient.
|
Send typing message to trigger a typing indicator for the recipient.
|
||||||
@ -786,16 +772,6 @@ Specify the recipient group ID in base64 encoding.
|
|||||||
*--delete*::
|
*--delete*::
|
||||||
Delete local group data completely after quitting group.
|
Delete local group data completely after quitting group.
|
||||||
|
|
||||||
=== terminateGroup
|
|
||||||
|
|
||||||
Permanently terminate a group for all members.
|
|
||||||
This requires admin privileges.
|
|
||||||
Afterwards no member (not even an admin) can send messages or start calls in the group; members keep access to the existing message history.
|
|
||||||
Only supported for Signal group v2 groups.
|
|
||||||
|
|
||||||
*-g* GROUP, *--group-id* GROUP::
|
|
||||||
Specify the group ID in base64 encoding.
|
|
||||||
|
|
||||||
=== listGroups
|
=== listGroups
|
||||||
|
|
||||||
Show a list of known groups and related information.
|
Show a list of known groups and related information.
|
||||||
@ -1192,29 +1168,13 @@ signal-cli -a ACCOUNT trust -a RECIPIENT
|
|||||||
* *3*: Server or IO error
|
* *3*: Server or IO error
|
||||||
* *4*: Sending failed due to untrusted key
|
* *4*: Sending failed due to untrusted key
|
||||||
* *5*: Server rate limiting error
|
* *5*: Server rate limiting error
|
||||||
* *6*: CAPTCHA was rejected
|
|
||||||
|
|
||||||
== Files
|
== Files
|
||||||
|
|
||||||
The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--data-dir* (legacy *--config*):
|
The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*:
|
||||||
|
|
||||||
`$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`)
|
`$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`)
|
||||||
|
|
||||||
=== Configuration file
|
|
||||||
|
|
||||||
signal-cli supports a JSON-based global configuration file that provides defaults for CLI options.
|
|
||||||
Keys use camelCase and generally match the long CLI parameter names (for example `dataDir`, `verbose`, `logFile`, `serviceEnvironment`, `trustNewIdentities`, `output`, `disableSendLog`, `account`).
|
|
||||||
|
|
||||||
Configuration files are read and merged in this order; later files override earlier ones:
|
|
||||||
|
|
||||||
- `/etc/signal-cli/config.json` (system-wide defaults)
|
|
||||||
- the path in the `SIGNAL_CLI_CONFIG` environment variable (if set)
|
|
||||||
- `$XDG_CONFIG_HOME/signal-cli/config.json` (per-user; defaults to `$HOME/.config/signal-cli/config.json`)
|
|
||||||
|
|
||||||
When multiple configuration files are present their settings are merged; values from later files override earlier values.
|
|
||||||
Command-line options always take precedence over configuration file values.
|
|
||||||
Overall precedence (highest → lowest): command-line options → per-user config → system config → built-in defaults.
|
|
||||||
|
|
||||||
== Authors
|
== Authors
|
||||||
|
|
||||||
Maintained by AsamK <asamk@gmx.de>, who is assisted by other open source contributors.
|
Maintained by AsamK <asamk@gmx.de>, who is assisted by other open source contributors.
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
# Reproducible builds
|
|
||||||
|
|
||||||
This process lets you verify that the version of signal-cli that was downloaded from the Github Releases matches the source code in the public repository.
|
|
||||||
|
|
||||||
This is achieved by replicating the build environment as Docker images.
|
|
||||||
|
|
||||||
Currently, only the following binaries are reproducible:
|
|
||||||
|
|
||||||
- [x] JAR package (`signal-cli-XXX.tar.gz`)
|
|
||||||
- [ ] Native binary (`signal-cli-XXX-Linux-native.tar.gz`)
|
|
||||||
- [x] Rust client binary (`signal-cli-XXX-Linux-client.tar.gz`)
|
|
||||||
|
|
||||||
In the following section, we will use signal-cli version 0.14.2 as the reference example. Simply replace all occurrences of 0.14.2 with the version number you are about to verify.
|
|
||||||
|
|
||||||
## Step-by-step instructions
|
|
||||||
|
|
||||||
### 0. Prerequisites
|
|
||||||
|
|
||||||
Before you begin, ensure you have the following installed:
|
|
||||||
|
|
||||||
- git
|
|
||||||
- docker (or podman)
|
|
||||||
|
|
||||||
### 1. Verifying reproducibility
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone --depth 1 --branch v0.14.2 https://github.com/AsamK/signal-cli
|
|
||||||
cd ./signal-cli
|
|
||||||
./reproducible-builds/verify.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
If each one ends with `... matches!` for every binary (except the native one for now), you're good to go! You've successfully verified that the Github Release binaries were built from exactly the same code as is in the signal-cli git repository.
|
|
||||||
|
|
||||||
If you get `... doesn't match!`, it means something went wrong (except for the native one for now). Please [open an issue](https://github.com/AsamK/signal-cli/issues/new/choose).
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
ARG ZULU_TAG="25-latest@sha256:8eca9375451a392bff01efe946f2e9263c50aa71a9d68423c068cc1061a41b7e"
|
|
||||||
|
|
||||||
FROM docker.io/azul/zulu-openjdk:$ZULU_TAG
|
|
||||||
ARG SOURCE_DATE_EPOCH="1776889382"
|
|
||||||
ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH
|
|
||||||
ENV LANG=C.UTF-8
|
|
||||||
ENV LC_CTYPE=en_US.UTF-8
|
|
||||||
RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \
|
|
||||||
&& sed -i 's/^deb /deb [snapshot=yes] /' /etc/apt/sources.list && apt update --snapshot "$SNAPSHOT" && apt install -y make asciidoc-base --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests
|
|
||||||
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
||||||
WORKDIR /signal-cli
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ]
|
|
||||||
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