mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-09-21 06:19:41 +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:
|
||||
- v*
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: write # to fetch code (actions/checkout) and create release
|
||||
|
||||
env:
|
||||
IMAGE_NAME: signal-cli
|
||||
IMAGE_REGISTRY: ghcr.io/asamk
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
REGISTRY_PASSWORD: ${{ github.token }}
|
||||
ARCHIVE_JAVA_VERSION: 25
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
|
||||
release:
|
||||
needs: build
|
||||
ci_wf:
|
||||
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
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
signal_cli_version: ${{ steps.cli_ver.outputs.version }}
|
||||
release_id: ${{ steps.create_release.outputs.id }}
|
||||
|
||||
steps:
|
||||
|
||||
- name: Download signal-cli build from CI workflow
|
||||
uses: actions/download-artifact@v8
|
||||
|
||||
- name: Get signal-cli version
|
||||
id: version
|
||||
id: cli_ver
|
||||
run: |
|
||||
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
||||
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
ver="${GITHUB_REF_NAME#v}"
|
||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create draft release and upload assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract archive
|
||||
run: |
|
||||
gh release create "v${{ steps.version.outputs.version }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--title "v${{ steps.version.outputs.version }}" \
|
||||
--draft \
|
||||
--verify-tag \
|
||||
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
|
||||
"signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \
|
||||
"signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \
|
||||
"signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz"
|
||||
tree .
|
||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
||||
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
|
||||
rm -rf signal-cli-archive-*/
|
||||
|
||||
# - name: Get signal-client jar version
|
||||
# id: lib_ver
|
||||
# run: |
|
||||
# JAR_PREFIX=libsignal-client-
|
||||
# jar_file=$(find ./signal-cli-*/lib/ -name "$JAR_PREFIX*.jar")
|
||||
# jar_version=$(echo "$jar_file" | xargs basename | sed "s/$JAR_PREFIX//; s/.jar//")
|
||||
# echo "$jar_version"
|
||||
# echo "signal_client_version=${jar_version}" >> $GITHUB_OUTPUT
|
||||
#
|
||||
# - name: Download signal-client builds
|
||||
# env:
|
||||
# RELEASES_URL: https://github.com/signalapp/libsignal/releases/download/
|
||||
# FILE_NAMES: signal_jni.dll libsignal_jni.dylib
|
||||
# SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
|
||||
# run: |
|
||||
# for file_name in $FILE_NAMES; do
|
||||
# curl -sOL "${RELEASES_URL}/v${SIGNAL_CLIENT_VER}/${file_name}" # note: added v
|
||||
# done
|
||||
# tree .
|
||||
|
||||
- name: Compress native app
|
||||
env:
|
||||
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
||||
run: |
|
||||
chmod +x signal-cli-native/signal-cli
|
||||
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-native.tar.gz -C signal-cli-native signal-cli
|
||||
rm -rf signal-cli-native/
|
||||
|
||||
- name: 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:
|
||||
needs: release
|
||||
needs: ci_wf
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
@ -60,23 +188,32 @@ jobs:
|
||||
- name: Download signal-cli build from CI workflow
|
||||
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
|
||||
run: |
|
||||
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}.tar.gz
|
||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
||||
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||
rm -r signal-cli-archive-* signal-cli-native
|
||||
mkdir -p build/install/
|
||||
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
|
||||
mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
uses: redhat-actions/buildah-build@v3
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: ${{ env.IMAGE_NAME }}
|
||||
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
||||
containerfiles: ./Containerfile
|
||||
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
|
||||
containerfiles:
|
||||
./Containerfile
|
||||
oci: true
|
||||
|
||||
- name: Push To GHCR
|
||||
uses: redhat-actions/push-to-registry@v3
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
id: push
|
||||
with:
|
||||
image: ${{ steps.build_image.outputs.image }}
|
||||
@ -90,9 +227,10 @@ jobs:
|
||||
echo "${{ toJSON(steps.push.outputs) }}"
|
||||
|
||||
build-container-native:
|
||||
needs: release
|
||||
needs: ci_wf
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
@ -100,24 +238,30 @@ jobs:
|
||||
- name: Download signal-cli build from CI workflow
|
||||
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
|
||||
run: |
|
||||
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
|
||||
mkdir -p build/native/nativeCompile/
|
||||
mv signal-cli build/native/nativeCompile/
|
||||
chmod +x build/native/nativeCompile/signal-cli
|
||||
chmod +x ./signal-cli-native/signal-cli
|
||||
mv ./signal-cli-native/signal-cli build/native/nativeCompile/
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
uses: redhat-actions/buildah-build@v3
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: ${{ env.IMAGE_NAME }}
|
||||
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
||||
containerfiles: ./native.Containerfile
|
||||
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
|
||||
containerfiles:
|
||||
./native.Containerfile
|
||||
oci: true
|
||||
|
||||
- name: Push To GHCR
|
||||
uses: redhat-actions/push-to-registry@v3
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
id: push
|
||||
with:
|
||||
image: ${{ steps.build_image.outputs.image }}
|
||||
@ -131,9 +275,10 @@ jobs:
|
||||
echo "${{ toJSON(steps.push.outputs) }}"
|
||||
|
||||
build-container-client:
|
||||
needs: release
|
||||
needs: ci_wf
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
@ -141,24 +286,30 @@ jobs:
|
||||
- name: Download signal-cli build from CI workflow
|
||||
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
|
||||
run: |
|
||||
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
|
||||
mkdir -p client/target/release/
|
||||
mv signal-cli-client client/target/release/
|
||||
chmod +x client/target/release/signal-cli-client
|
||||
chmod +x ./signal-cli-client-ubuntu/signal-cli-client
|
||||
mv ./signal-cli-client-ubuntu/signal-cli-client client/target/release/
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
uses: redhat-actions/buildah-build@v3
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: ${{ env.IMAGE_NAME }}
|
||||
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
||||
containerfiles: ./client.Containerfile
|
||||
tags: latest-client ${{ github.sha }}-client ${{ steps.cli_ver.outputs.version }}-client
|
||||
containerfiles:
|
||||
./client.Containerfile
|
||||
oci: true
|
||||
|
||||
- name: Push To GHCR
|
||||
uses: redhat-actions/push-to-registry@v3
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
id: push
|
||||
with:
|
||||
image: ${{ steps.build_image.outputs.image }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
.gradle/
|
||||
.kotlin/
|
||||
.idea/*
|
||||
!.idea/codeStyles/
|
||||
build/
|
||||
@ -14,10 +13,3 @@ out/
|
||||
.DS_Store
|
||||
/bin/
|
||||
/test-config/
|
||||
/dist/
|
||||
/github/
|
||||
man/*.1
|
||||
man/*.5
|
||||
man/man1
|
||||
man/man5
|
||||
.superpowers/
|
||||
|
||||
99
CHANGELOG.md
99
CHANGELOG.md
@ -1,104 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- `send --attachment-dimensions` and `--attachment-blurhash` to set the placeholder shown before an attachment is downloaded
|
||||
|
||||
## [0.14.8] - 2026-09-10
|
||||
|
||||
### Added
|
||||
|
||||
- Add terminateGroup command to terminate a group for everyone
|
||||
- Include isVoiceNote in receive JSON and JSON-RPC attachment payloads
|
||||
|
||||
### Improved
|
||||
|
||||
- Prevent more storage sync loops
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix graalvm issue with image dimension probing
|
||||
- Fix issue with sticker storage sync
|
||||
|
||||
## [0.14.7] - 2026-08-01
|
||||
|
||||
### Added
|
||||
|
||||
- Sync installed sticker packs via storage sync
|
||||
|
||||
### Improved
|
||||
|
||||
- Set width/height for outgoing image attachments (Thanks @mikesimone)
|
||||
- Update to new registration/provisioning API
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removing self from pending group invite now works if invited via PNI
|
||||
- Correctly set needs PNI signature for recipients
|
||||
- Fix linking after previous unsuccessful link attempt
|
||||
|
||||
## [0.14.6] - 2026-07-12
|
||||
|
||||
### Added
|
||||
|
||||
- New `sendStory` command to post file attachment stories to "My Story" or to a group via `--group-id`
|
||||
|
||||
### Improved
|
||||
|
||||
- The account parameter `-a` now supports ACI in addition to phone number
|
||||
- Disabling read receipts in configuration now prevents sending read receipts (only sync message to linked devices is still sent)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Sending to large groups is no longer slowed down by members that are already known to be unregistered; they are skipped instead of being retried via the legacy 1:1 send path on every send.
|
||||
|
||||
## [0.14.5] - 2026-06-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Disable host validation when binding on 0.0.0.0
|
||||
- Use new SVR2 enclave for PINs
|
||||
|
||||
### Fixed
|
||||
|
||||
- Receiving unidentified sender messages after signal server change
|
||||
|
||||
## [0.14.4] - 2026-05-23
|
||||
|
||||
### Added
|
||||
|
||||
- Support for a global configuration file to set system-wide defaults
|
||||
|
||||
### Fixed
|
||||
|
||||
- Group admins can now see profile information for users requesting to join groups.
|
||||
- Storage sync with unregistered contacts fixed
|
||||
- Incoming messages are validated more accurately, fixing receiving messages from new contacts
|
||||
|
||||
### Improved
|
||||
|
||||
- Some security and stability improvements, including HTTP HOST header validation and safer temporary file handling.
|
||||
|
||||
## [0.14.3] - 2026-04-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix sender key re-distribution on every group message (Thanks @meinecke)
|
||||
|
||||
### Improved
|
||||
|
||||
- Performance improvement when assigning admin role to multiple group members
|
||||
- Increase disconnect timeout for websocket connections
|
||||
- Release builds are now reproducible
|
||||
|
||||
### Changed
|
||||
|
||||
- Send message results now surface server-advised retry time for plain rate-limit (HTTP 413) failures, not only for proof-required challenges. The `retryAfterSeconds` field in JSON-RPC `SendMessageResult` is populated whenever the server sends a `Retry-After` header. The canonical way to distinguish proof-required failures remains `token != null`. Text output includes "retry after N seconds" when known.
|
||||
- Add distinct JSON-RPC error code (6) for captcha rejection (Thanks @tonycpsu)
|
||||
- No longer sends busy call response to allow linked devices to accept call
|
||||
|
||||
## [0.14.2] - 2026-04-04
|
||||
|
||||
### Added
|
||||
|
||||
29
README.md
29
README.md
@ -3,9 +3,8 @@
|
||||
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
||||
It supports registering, verifying, sending and receiving messages.
|
||||
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
||||
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/lib/libsignal-service).
|
||||
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal-service).
|
||||
For registering you need a phone number where you can receive SMS or incoming calls.
|
||||
Existing accounts without a phone number can be linked from a compatible Signal mobile app.
|
||||
|
||||
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
||||
For this use-case, it has a daemon mode with JSON-RPC interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.5.adoc))
|
||||
@ -61,26 +60,12 @@ For a complete usage overview please read
|
||||
the [man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.adoc) and
|
||||
the [wiki](https://github.com/AsamK/signal-cli/wiki).
|
||||
|
||||
For a numbered account, ACCOUNT is your phone number in international format and must include the country calling code. Hence it
|
||||
Important: The ACCOUNT is your phone number in international format and must include the country calling code. Hence it
|
||||
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
|
||||
of all country codes.)
|
||||
For a linked account without a phone number, use its ACI (Account ID) instead.
|
||||
See the [wiki](https://github.com/AsamK/signal-cli/wiki) for further documentation.
|
||||
|
||||
* Link to an existing account
|
||||
|
||||
If you have an existing Signal account, with or without a phone number,
|
||||
you can link signal-cli to it with:
|
||||
|
||||
signal-cli link
|
||||
|
||||
* Register a number (with SMS verification)
|
||||
|
||||
Alternatively, if you don't have an existing Signal
|
||||
account, you can register one from signal-cli. Note
|
||||
that this will unregister any existing client
|
||||
associated with the same number.
|
||||
|
||||
signal-cli -a ACCOUNT register
|
||||
|
||||
You can register Signal using a landline number. In this case, you need to follow the procedure below:
|
||||
@ -163,16 +148,6 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
|
||||
./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)
|
||||
|
||||
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 {
|
||||
java
|
||||
application
|
||||
eclipse
|
||||
`check-lib-versions`
|
||||
id("org.graalvm.buildtools.native") version "1.1.12"
|
||||
id("org.graalvm.buildtools.native") version "1.0.0"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "org.asamk"
|
||||
version = "0.14.9-SNAPSHOT"
|
||||
version = "0.14.2"
|
||||
}
|
||||
|
||||
java {
|
||||
@ -74,11 +72,6 @@ val excludePatterns = mapOf(
|
||||
)
|
||||
)
|
||||
|
||||
val schemaAnnotationProcessor = configurations.create("schemaAnnotationProcessor") {
|
||||
isCanBeConsumed = false
|
||||
isCanBeResolved = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
registerTransform(JarFileExcluder::class) {
|
||||
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.jackson.databind)
|
||||
implementation(libs.argparse4j)
|
||||
@ -99,10 +90,6 @@ dependencies {
|
||||
implementation(libs.slf4j.jul)
|
||||
implementation(libs.logback)
|
||||
implementation(libs.zxing)
|
||||
implementation(libs.micronaut.json.schema.annotations)
|
||||
if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) {
|
||||
implementation(libs.micronaut.json.schema.generator)
|
||||
}
|
||||
implementation(project(":libsignal-cli"))
|
||||
|
||||
testImplementation(libs.junit.jupiter)
|
||||
@ -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 {
|
||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_25)
|
||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_24)
|
||||
}
|
||||
|
||||
java {
|
||||
targetCompatibility = JavaVersion.VERSION_25
|
||||
targetCompatibility = JavaVersion.VERSION_24
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
538
client/Cargo.lock
generated
538
client/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "signal-cli-client"
|
||||
version = "0.14.8"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -45,24 +45,6 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="0.14.8" date="2026-09-10">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.8</url>
|
||||
</release>
|
||||
<release version="0.14.7" date="2026-08-01">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.7</url>
|
||||
</release>
|
||||
<release version="0.14.6" date="2026-07-12">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.6</url>
|
||||
</release>
|
||||
<release version="0.14.5" date="2026-06-11">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.5</url>
|
||||
</release>
|
||||
<release version="0.14.4" date="2026-05-23">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
|
||||
</release>
|
||||
<release version="0.14.3" date="2026-04-22">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.3</url>
|
||||
</release>
|
||||
<release version="0.14.2" date="2026-04-04">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
|
||||
</release>
|
||||
|
||||
@ -1,29 +1,20 @@
|
||||
[versions]
|
||||
slf4j = "2.0.19"
|
||||
coroutines = "1.11.0"
|
||||
junit = "6.1.3"
|
||||
micronaut-json-schema = "2.2.0"
|
||||
micronaut-core = "5.2.0"
|
||||
signal-service = "2.15.3_unofficial_153"
|
||||
slf4j = "2.0.17"
|
||||
junit = "6.0.2"
|
||||
|
||||
[libraries]
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85.2"
|
||||
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.2"
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83"
|
||||
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.20.2"
|
||||
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
|
||||
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
|
||||
zxing = "com.google.zxing:core:3.5.4"
|
||||
micronaut-json-schema-annotations = { module = "io.micronaut.jsonschema:micronaut-json-schema-annotations", version.ref = "micronaut-json-schema" }
|
||||
micronaut-json-schema-processor = { module = "io.micronaut.jsonschema:micronaut-json-schema-processor", version.ref = "micronaut-json-schema" }
|
||||
micronaut-json-schema-generator = { module = "io.micronaut.jsonschema:micronaut-json-schema-generator", version.ref = "micronaut-json-schema" }
|
||||
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-core" }
|
||||
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
||||
logback = "ch.qos.logback:logback-classic:1.6.3"
|
||||
logback = "ch.qos.logback:logback-classic:1.5.32"
|
||||
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
|
||||
sqlite = "org.xerial:sqlite-jdbc:3.53.4.0"
|
||||
hikari = "com.zaxxer:HikariCP:7.1.0"
|
||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_142"
|
||||
sqlite = "org.xerial:sqlite-jdbc:3.51.2.0"
|
||||
hikari = "com.zaxxer:HikariCP:7.0.2"
|
||||
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||
junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,9 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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:
|
||||
#
|
||||
@ -29,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh gradlew
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# 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
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem gradlew startup script for Windows
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@ -51,7 +51,7 @@ echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
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 location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
||||
@ -18,9 +18,9 @@ val libsignalClientPath = project.findProperty("libsignal_client_path")?.toStrin
|
||||
|
||||
dependencies {
|
||||
if (libsignalClientPath == null) {
|
||||
implementation(libs.signalnetwork)
|
||||
implementation(libs.signalservice)
|
||||
} else {
|
||||
implementation(libs.signalnetwork) {
|
||||
implementation(libs.signalservice) {
|
||||
exclude(group = "org.signal", module = "libsignal-client")
|
||||
}
|
||||
implementation(files(libsignalClientPath))
|
||||
@ -30,7 +30,6 @@ dependencies {
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.sqlite)
|
||||
implementation(libs.hikari)
|
||||
compileOnly(libs.kotlinx.coroutines.core)
|
||||
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testImplementation(platform(libs.junit.jupiter.bom))
|
||||
|
||||
@ -54,7 +54,6 @@ import org.asamk.signal.manager.api.UserStatus;
|
||||
import org.asamk.signal.manager.api.UsernameLinkUrl;
|
||||
import org.asamk.signal.manager.api.UsernameStatus;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
import org.signal.core.util.UuidUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -75,10 +74,6 @@ public interface Manager extends Closeable {
|
||||
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
||||
}
|
||||
|
||||
static boolean isValidAci(final String aci) {
|
||||
return UuidUtil.INSTANCE.isUuid(aci);
|
||||
}
|
||||
|
||||
static boolean isSignalClientAvailable() {
|
||||
final Logger logger = LoggerFactory.getLogger(Manager.class);
|
||||
try {
|
||||
@ -96,16 +91,6 @@ public interface Manager extends Closeable {
|
||||
|
||||
String getSelfNumber();
|
||||
|
||||
String getSelfACI();
|
||||
|
||||
/**
|
||||
* Returns the phone number for numbered accounts, or the ACI for numberless accounts.
|
||||
*/
|
||||
default String getSelfIdentifier() {
|
||||
final var number = getSelfNumber();
|
||||
return number != null ? number : getSelfACI();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used for checking a set of phone numbers for registration on Signal
|
||||
*
|
||||
@ -192,10 +177,6 @@ public interface Manager extends Closeable {
|
||||
|
||||
void deleteGroup(GroupId groupId) throws IOException;
|
||||
|
||||
SendGroupMessageResults terminateGroup(
|
||||
GroupId groupId
|
||||
) throws IOException, GroupNotFoundException, NotAGroupMemberException;
|
||||
|
||||
Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String name,
|
||||
Set<RecipientIdentifier.Single> members,
|
||||
@ -232,19 +213,6 @@ public interface Manager extends Closeable {
|
||||
long editTargetTimestamp
|
||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||
|
||||
/**
|
||||
* Post a file attachment story to "My Story" or to a group.
|
||||
*
|
||||
* @param attachment path to the file to upload and post as a story
|
||||
* @param allowsReplies whether other users are allowed to reply to this story
|
||||
* @param groupId if present, post the story to this group instead of "My Story"
|
||||
*/
|
||||
SendMessageResults sendStory(
|
||||
String attachment,
|
||||
boolean allowsReplies,
|
||||
Optional<GroupId> groupId
|
||||
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException;
|
||||
|
||||
SendMessageResults sendRemoteDeleteMessage(
|
||||
long targetSentTimestamp,
|
||||
Set<RecipientIdentifier> recipients
|
||||
@ -291,7 +259,7 @@ public interface Manager extends Closeable {
|
||||
RecipientIdentifier.Single recipient
|
||||
) throws IOException;
|
||||
|
||||
void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
||||
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
||||
|
||||
SendMessageResults sendMessageRequestResponse(
|
||||
MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
||||
|
||||
@ -8,6 +8,8 @@ import java.util.function.Consumer;
|
||||
|
||||
public interface MultiAccountManager extends AutoCloseable {
|
||||
|
||||
List<String> getAccountNumbers();
|
||||
|
||||
List<Manager> getManagers();
|
||||
|
||||
void addOnManagerAddedHandler(Consumer<Manager> handler);
|
||||
|
||||
@ -10,8 +10,5 @@ public interface ProvisioningManager {
|
||||
|
||||
URI getDeviceLinkUri() throws TimeoutException, IOException;
|
||||
|
||||
/**
|
||||
* Completes linking and returns the account's phone number, or ACI if it has no number.
|
||||
*/
|
||||
String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException;
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.api.TotpRequiredException;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
|
||||
import java.io.Closeable;
|
||||
@ -18,19 +17,13 @@ public interface RegistrationManager extends Closeable {
|
||||
boolean voiceVerification,
|
||||
String captcha,
|
||||
final boolean forceRegister
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, TotpRequiredException, VerificationMethodNotAvailableException;
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
|
||||
|
||||
void verifyAccount(
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
|
||||
|
||||
void registerWithRecoveryKey(
|
||||
String recoveryKey,
|
||||
boolean forceRegister,
|
||||
Integer totp
|
||||
) throws IOException, RateLimitException, TotpRequiredException;
|
||||
|
||||
void deleteLocalAccountData() throws IOException;
|
||||
|
||||
boolean isRegistered();
|
||||
|
||||
@ -15,7 +15,6 @@ import org.asamk.signal.manager.internal.RegistrationManagerImpl;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||
@ -25,7 +24,6 @@ import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SignalAccountFiles {
|
||||
|
||||
@ -66,26 +64,15 @@ public class SignalAccountFiles {
|
||||
return accountsStore.getAllNumbers();
|
||||
}
|
||||
|
||||
public Set<String> getAllLocalAccountIdentifiers() throws IOException {
|
||||
return accountsStore.getAllAccounts()
|
||||
.stream()
|
||||
.map(a -> a.number() != null ? a.number() : a.uuid())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||
final var identifier = a.number() != null ? a.number() : a.uuid();
|
||||
try {
|
||||
final var manager = a.number() != null
|
||||
? initManagerByNumber(a.number(), a.path())
|
||||
: initManagerByAci(ACI.parseOrThrow(a.uuid()), a.path());
|
||||
return new Pair<Manager, Throwable>(manager, null);
|
||||
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
|
||||
} catch (NotRegisteredException e) {
|
||||
logger.warn("Ignoring {}: {} ({})", identifier, e.getMessage(), e.getClass().getSimpleName());
|
||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||
return null;
|
||||
} catch (AccountCheckException | IOException e) {
|
||||
logger.error("Failed to load {}: {} ({})", identifier, e.getMessage(), e.getClass().getSimpleName());
|
||||
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||
return new Pair<Manager, Throwable>(null, e);
|
||||
}
|
||||
}).filter(Objects::nonNull).toList();
|
||||
@ -103,31 +90,15 @@ public class SignalAccountFiles {
|
||||
return new MultiAccountManagerImpl(managers, this);
|
||||
}
|
||||
|
||||
public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var accountPath = accountsStore.getPathByNumber(number);
|
||||
return this.initManagerByNumber(number, accountPath);
|
||||
return this.initManager(number, accountPath);
|
||||
}
|
||||
|
||||
public Manager initManagerByAci(String aciStr) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var aci = ACI.parseOrThrow(aciStr);
|
||||
final var accountPath = accountsStore.getPathByAci(aci);
|
||||
return this.initManagerByAci(aci, accountPath);
|
||||
}
|
||||
|
||||
private Manager initManagerByNumber(
|
||||
private Manager initManager(
|
||||
String number,
|
||||
String accountPath
|
||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var account = loadAccount(accountPath);
|
||||
if (!number.equals(account.getNumber())) {
|
||||
account.close();
|
||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||
}
|
||||
|
||||
return initManagerFromAccount(number, accountPath, account);
|
||||
}
|
||||
|
||||
private SignalAccount loadAccount(final String accountPath) throws NotRegisteredException, IOException {
|
||||
if (accountPath == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
@ -135,27 +106,12 @@ public class SignalAccountFiles {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
|
||||
return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||
}
|
||||
|
||||
private Manager initManagerByAci(
|
||||
ACI aci,
|
||||
String accountPath
|
||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var account = loadAccount(accountPath);
|
||||
if (!aci.equals(account.getAci())) {
|
||||
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||
if (!number.equals(account.getNumber())) {
|
||||
account.close();
|
||||
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci());
|
||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||
}
|
||||
|
||||
return initManagerFromAccount(aci.toString(), accountPath, account);
|
||||
}
|
||||
|
||||
private ManagerImpl initManagerFromAccount(
|
||||
final String identifier,
|
||||
final String accountPath,
|
||||
final SignalAccount account
|
||||
) throws NotRegisteredException, IOException, AccountCheckException {
|
||||
if (!account.isRegistered()) {
|
||||
account.close();
|
||||
throw new NotRegisteredException();
|
||||
@ -180,7 +136,7 @@ public class SignalAccountFiles {
|
||||
throw new IOException("signal-cli version is too old for the Signal-Server, please update.");
|
||||
} catch (IOException e) {
|
||||
manager.close();
|
||||
throw new AccountCheckException("Error while checking account " + identifier + ": " + e.getMessage(), e);
|
||||
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
if (account.getServiceEnvironment() == null) {
|
||||
@ -211,11 +167,6 @@ public class SignalAccountFiles {
|
||||
String number,
|
||||
Consumer<Manager> newManagerListener
|
||||
) throws IOException {
|
||||
final var aci = ACI.parseOrNull(number);
|
||||
if (aci != null) {
|
||||
return initRegistrationManager(aci, newManagerListener);
|
||||
}
|
||||
|
||||
final var accountPath = accountsStore.getPathByNumber(number);
|
||||
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
||||
final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
|
||||
@ -255,43 +206,4 @@ public class SignalAccountFiles {
|
||||
newManagerListener,
|
||||
new AccountFileUpdaterImpl(accountsStore, accountPath));
|
||||
}
|
||||
|
||||
private RegistrationManager initRegistrationManager(
|
||||
final ACI aci,
|
||||
final Consumer<Manager> newManagerListener
|
||||
) throws IOException {
|
||||
final var accountPath = accountsStore.getPathByAci(aci);
|
||||
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
||||
final var newAccountPath = accountPath == null ? accountsStore.addAccount(null, aci) : accountPath;
|
||||
final var account = SignalAccount.create(pathConfig.dataPath(),
|
||||
newAccountPath,
|
||||
null,
|
||||
aci,
|
||||
serviceEnvironment,
|
||||
KeyUtils.generateIdentityKeyPair(),
|
||||
KeyUtils.generateIdentityKeyPair(),
|
||||
KeyUtils.createProfileKey(),
|
||||
settings);
|
||||
account.initDatabase();
|
||||
return new RegistrationManagerImpl(account,
|
||||
pathConfig,
|
||||
serviceEnvironmentConfig,
|
||||
userAgent,
|
||||
newManagerListener,
|
||||
new AccountFileUpdaterImpl(accountsStore, newAccountPath));
|
||||
}
|
||||
|
||||
final var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||
if (!aci.equals(account.getAci())) {
|
||||
account.close();
|
||||
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci());
|
||||
}
|
||||
account.initDatabase();
|
||||
return new RegistrationManagerImpl(account,
|
||||
pathConfig,
|
||||
serviceEnvironmentConfig,
|
||||
userAgent,
|
||||
newManagerListener,
|
||||
new AccountFileUpdaterImpl(accountsStore, accountPath));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.signal.libsignal.net.BadRequestError;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class BadRequestException extends IOException {
|
||||
|
||||
private final BadRequestError error;
|
||||
|
||||
public BadRequestException(final BadRequestError error) {
|
||||
super(error.toString());
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public BadRequestError getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
public record CallOffer(
|
||||
long callId, Type type, byte[] opaque
|
||||
long callId,
|
||||
Type type,
|
||||
byte[] opaque
|
||||
) {
|
||||
|
||||
public enum Type {
|
||||
|
||||
@ -2,11 +2,11 @@ package org.asamk.signal.manager.api;
|
||||
|
||||
public class CaptchaRequiredException extends Exception {
|
||||
|
||||
private long nextVerificationAttemptMilliseconds;
|
||||
private long nextAttemptTimestamp;
|
||||
|
||||
public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) {
|
||||
public CaptchaRequiredException(final long nextAttemptTimestamp) {
|
||||
super("Captcha required");
|
||||
this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds;
|
||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
||||
}
|
||||
|
||||
public CaptchaRequiredException(final String message) {
|
||||
@ -17,7 +17,7 @@ public class CaptchaRequiredException extends Exception {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public long getNextVerificationAttemptMilliseconds() {
|
||||
return nextVerificationAttemptMilliseconds;
|
||||
public long getNextAttemptTimestamp() {
|
||||
return nextAttemptTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ public record Contact(
|
||||
long muteUntil,
|
||||
boolean hideStory,
|
||||
boolean isBlocked,
|
||||
long blockedAt,
|
||||
boolean isArchived,
|
||||
boolean isProfileSharingEnabled,
|
||||
boolean isHidden,
|
||||
@ -35,7 +34,6 @@ public record Contact(
|
||||
builder.muteUntil,
|
||||
builder.hideStory,
|
||||
builder.isBlocked,
|
||||
builder.blockedAt,
|
||||
builder.isArchived,
|
||||
builder.isProfileSharingEnabled,
|
||||
builder.isHidden,
|
||||
@ -60,7 +58,6 @@ public record Contact(
|
||||
builder.muteUntil = copy.muteUntil();
|
||||
builder.hideStory = copy.hideStory();
|
||||
builder.isBlocked = copy.isBlocked();
|
||||
builder.blockedAt = copy.blockedAt();
|
||||
builder.isArchived = copy.isArchived();
|
||||
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
||||
builder.isHidden = copy.isHidden();
|
||||
@ -83,21 +80,6 @@ public record Contact(
|
||||
return givenName + " " + familyName;
|
||||
}
|
||||
|
||||
public String getDisplayNickname() {
|
||||
final var noNickGivenName = Util.isEmpty(nickNameGivenName);
|
||||
final var noNickFamilyName = Util.isEmpty(nickNameFamilyName);
|
||||
|
||||
if (noNickGivenName && noNickFamilyName) {
|
||||
return null;
|
||||
} else if (noNickGivenName) {
|
||||
return nickNameFamilyName;
|
||||
} else if (noNickFamilyName) {
|
||||
return nickNameGivenName;
|
||||
}
|
||||
|
||||
return nickNameGivenName + " " + nickNameFamilyName;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String givenName;
|
||||
@ -112,7 +94,6 @@ public record Contact(
|
||||
private long muteUntil;
|
||||
private boolean hideStory;
|
||||
private boolean isBlocked;
|
||||
private long blockedAt;
|
||||
private boolean isArchived;
|
||||
private boolean isProfileSharingEnabled;
|
||||
private boolean isHidden;
|
||||
@ -181,20 +162,10 @@ public record Contact(
|
||||
}
|
||||
|
||||
public Builder withIsBlocked(final boolean val) {
|
||||
if (val && !isBlocked) {
|
||||
blockedAt = System.currentTimeMillis();
|
||||
} else if (!val) {
|
||||
blockedAt = 0;
|
||||
}
|
||||
isBlocked = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withBlockedAt(final long val) {
|
||||
blockedAt = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withIsArchived(final boolean val) {
|
||||
isArchived = val;
|
||||
return this;
|
||||
|
||||
@ -22,8 +22,7 @@ public record Group(
|
||||
GroupPermission permissionEditDetails,
|
||||
GroupPermission permissionSendMessage,
|
||||
boolean isMember,
|
||||
boolean isAdmin,
|
||||
boolean isTerminated
|
||||
boolean isAdmin
|
||||
) {
|
||||
|
||||
public static Group from(
|
||||
@ -60,7 +59,6 @@ public record Group(
|
||||
groupInfo.getPermissionEditDetails(),
|
||||
groupInfo.getPermissionSendMessage(),
|
||||
groupInfo.isMember(selfRecipientId),
|
||||
groupInfo.isAdmin(selfRecipientId),
|
||||
groupInfo.isTerminated());
|
||||
groupInfo.isAdmin(selfRecipientId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,6 @@ import java.util.Optional;
|
||||
public record Message(
|
||||
String messageText,
|
||||
List<String> attachments,
|
||||
List<AttachmentDimensions> attachmentDimensions,
|
||||
List<String> attachmentBlurHashes,
|
||||
boolean viewOnce,
|
||||
boolean voiceNote,
|
||||
List<Mention> mentions,
|
||||
@ -19,8 +17,6 @@ public record Message(
|
||||
boolean urgent
|
||||
) {
|
||||
|
||||
public record AttachmentDimensions(int width, int height) {}
|
||||
|
||||
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
||||
|
||||
public record Quote(
|
||||
|
||||
@ -4,6 +4,7 @@ import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.asamk.signal.manager.util.MimeUtils;
|
||||
import org.signal.core.models.ServiceId;
|
||||
import org.signal.libsignal.metadata.ProtocolException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
@ -156,7 +157,7 @@ public record MessageEnvelope(
|
||||
dataMessage.getExpiresInSeconds(),
|
||||
dataMessage.isExpirationUpdate(),
|
||||
dataMessage.isViewOnce(),
|
||||
false,
|
||||
dataMessage.isEndSession(),
|
||||
dataMessage.isProfileKeyUpdate(),
|
||||
dataMessage.getProfileKey().isPresent(),
|
||||
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
|
||||
@ -739,10 +740,7 @@ public record MessageEnvelope(
|
||||
null,
|
||||
d.getE164(),
|
||||
null))
|
||||
.toList(),
|
||||
blockedListMessage.groups.stream()
|
||||
.map(group -> GroupId.unknownVersion(group.getGroupId()))
|
||||
.toList());
|
||||
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1030,7 +1028,7 @@ public record MessageEnvelope(
|
||||
final AttachmentFileProvider fileProvider,
|
||||
Exception exception
|
||||
) {
|
||||
final var serviceId = envelope.getSourceServiceId();
|
||||
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
|
||||
final var source = !envelope.isUnidentifiedSender() && serviceId != null
|
||||
? recipientResolver.resolveRecipient(serviceId)
|
||||
: envelope.isUnidentifiedSender() && content != null
|
||||
|
||||
@ -10,19 +10,12 @@ public class ProofRequiredException extends Exception {
|
||||
|
||||
private final String token;
|
||||
private final Set<Option> options;
|
||||
private final long retryAfterMilliseconds;
|
||||
private final long retryAfterSeconds;
|
||||
|
||||
public ProofRequiredException(final String token, final Set<Option> options, final long retryAfterMilliseconds) {
|
||||
super("Rate limit");
|
||||
this.token = token;
|
||||
this.options = options;
|
||||
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
||||
}
|
||||
|
||||
public static ProofRequiredException from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
|
||||
return new ProofRequiredException(e.getToken(),
|
||||
e.getOptions().stream().map(Option::from).collect(Collectors.toSet()),
|
||||
e.getRetryAfterSeconds() * 1000L);
|
||||
public ProofRequiredException(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
|
||||
this.token = e.getToken();
|
||||
this.options = e.getOptions().stream().map(Option::from).collect(Collectors.toSet());
|
||||
this.retryAfterSeconds = e.getRetryAfterSeconds();
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
@ -33,8 +26,8 @@ public class ProofRequiredException extends Exception {
|
||||
return options;
|
||||
}
|
||||
|
||||
public long getRetryAfterMilliseconds() {
|
||||
return retryAfterMilliseconds;
|
||||
public long getRetryAfterSeconds() {
|
||||
return retryAfterSeconds;
|
||||
}
|
||||
|
||||
public enum Option {
|
||||
|
||||
@ -2,18 +2,14 @@ package org.asamk.signal.manager.api;
|
||||
|
||||
public class RateLimitException extends Exception {
|
||||
|
||||
private final Long retryAfterMilliseconds;
|
||||
private final long nextAttemptTimestamp;
|
||||
|
||||
public RateLimitException(final Long retryAfterMilliseconds) {
|
||||
public RateLimitException(final long nextAttemptTimestamp) {
|
||||
super("Rate limit");
|
||||
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
||||
}
|
||||
|
||||
public static RateLimitException from(org.whispersystems.signalservice.api.push.exceptions.RateLimitException e) {
|
||||
return new RateLimitException(e.getRetryAfterMilliseconds().orElse(null));
|
||||
}
|
||||
|
||||
public Long getRetryAfterMilliseconds() {
|
||||
return retryAfterMilliseconds;
|
||||
public long getNextAttemptTimestamp() {
|
||||
return nextAttemptTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
public record ReceiveConfig(
|
||||
boolean ignoreAttachments,
|
||||
boolean ignoreStories,
|
||||
boolean ignoreAvatars,
|
||||
boolean ignoreStickers,
|
||||
boolean sendReadReceipts
|
||||
) {}
|
||||
public record ReceiveConfig(boolean ignoreAttachments, boolean ignoreStories, boolean ignoreAvatars, boolean ignoreStickers, boolean sendReadReceipts) {}
|
||||
|
||||
@ -9,13 +9,13 @@ public record SendMessageResult(
|
||||
boolean isNetworkFailure,
|
||||
boolean isUnregisteredFailure,
|
||||
boolean isIdentityFailure,
|
||||
RateLimitException rateLimitException,
|
||||
boolean isRateLimitFailure,
|
||||
ProofRequiredException proofRequiredFailure,
|
||||
boolean isInvalidPreKeyFailure
|
||||
) {
|
||||
|
||||
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(
|
||||
@ -23,30 +23,16 @@ public record SendMessageResult(
|
||||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver
|
||||
) {
|
||||
final var rateLimitFailure = sendMessageResult.getRateLimitFailure();
|
||||
final var proofRequiredFailure = sendMessageResult.getProofRequiredFailure();
|
||||
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||
sendMessageResult.getAddress())).toApiRecipientAddress(),
|
||||
sendMessageResult.isSuccess(),
|
||||
sendMessageResult.isNetworkFailure(),
|
||||
sendMessageResult.isUnregisteredFailure(),
|
||||
sendMessageResult.getIdentityFailure() != null,
|
||||
rateLimitFailure == null ? null : RateLimitException.from(rateLimitFailure),
|
||||
proofRequiredFailure == null ? null : ProofRequiredException.from(proofRequiredFailure),
|
||||
sendMessageResult.getRateLimitFailure() != null || sendMessageResult.getProofRequiredFailure() != null,
|
||||
sendMessageResult.getProofRequiredFailure() == null
|
||||
? null
|
||||
: new ProofRequiredException(sendMessageResult.getProofRequiredFailure()),
|
||||
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.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
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))
|
||||
.allMatch(r -> r) && results.values().stream().mapToInt(List::size).sum() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Longest rate-limit retry-after window across all rate-limited recipients, in milliseconds.
|
||||
* Null when no recipient reported one (server omitted Retry-After, or no rate-limit failures).
|
||||
*/
|
||||
public Long maxRateLimitRetryAfterMilliseconds() {
|
||||
return results.values()
|
||||
.stream()
|
||||
.flatMap(List::stream)
|
||||
.map(SendMessageResult::rateLimitRetryAfterMilliseconds)
|
||||
.filter(Objects::nonNull)
|
||||
.max(Long::compareTo)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class TotpRequiredException extends IOException {
|
||||
|
||||
public TotpRequiredException() {
|
||||
super("A TOTP token is required or the supplied token is incorrect");
|
||||
}
|
||||
}
|
||||
@ -3,5 +3,8 @@ package org.asamk.signal.manager.api;
|
||||
import java.util.List;
|
||||
|
||||
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.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.network.config.HttpProxy;
|
||||
import org.signal.network.config.SignalCdnUrl;
|
||||
import org.signal.network.config.SignalCdsiUrl;
|
||||
import org.signal.network.config.SignalProxy;
|
||||
import org.signal.network.config.SignalServiceConfiguration;
|
||||
import org.signal.network.config.SignalServiceUrl;
|
||||
import org.signal.network.config.SignalStorageUrl;
|
||||
import org.signal.network.config.SignalSvr2Url;
|
||||
import org.signal.network.config.TrustStore;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
@ -30,8 +30,7 @@ class LiveConfig {
|
||||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
||||
.decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6");
|
||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||
private static final String SVR2_MRENCLAVE = "fdbbacdc0c043d0d53fe1440f62728de0386f45ab0a275bd8f99e03a02af355e";
|
||||
private static final String SVR2_MRENCLAVE_LEGACY = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4";
|
||||
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
|
||||
|
||||
private static final String URL = "https://chat.signal.org";
|
||||
private static final String CDN_URL = "https://cdn.signal.org";
|
||||
@ -49,10 +48,10 @@ class LiveConfig {
|
||||
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==");
|
||||
private static final byte[] genericServerPublicParams = Base64.getDecoder()
|
||||
.decode("AeCO67P9mIv1yUHkdeZ9JF789GDbox61GvTqq3S4kYc1ADUWxWHQygU390tv1oRWt9WjkdZlU7mKkifF59ftjE+2ZlMmxns6I+ySiLpR8FEmfu+TGpVp3zYTjNV93obJJTyBCCsSHVETCyQRbKdCyb5TMa6LGrvcZaX0Q/VAavhuNA/m4kSiRMgSnYrUjGhVekdDnF+7xioo4wvFnxjIDh7uJQrYOWD6MloNGX7St5gbysTuQQ7i/HI38b9V8x8mKazuDSXxB//BKGZx/XHkK8cHX+QK1MPxYUVM1/CBI5oW");
|
||||
.decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN");
|
||||
|
||||
private static final byte[] backupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AZwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1Y4pyiScu0/S10n647ipo+iq97JZQv+UOlwH8ThyNlGT5DfxXCwTqivxHuXvZpuezPgHk5Gxl5aC6xuNxOnwmFlmu4CeSgdhW8+Pp0vAJOQ1MsU2D0+/kzI+tU94nB3tybY/Ao1AcGW2q41uKQbnOJUWwmQaFT6s+xTISgzsg7CPox6oORGX8rnyk/9lic3DbGsUHctIVpMAl/ogJBb4aYC");
|
||||
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
|
||||
|
||||
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
|
||||
|
||||
@ -94,7 +93,7 @@ class LiveConfig {
|
||||
createDefaultServiceConfiguration(interceptors),
|
||||
getUnidentifiedSenderTrustRoots(),
|
||||
CDSI_MRENCLAVE,
|
||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
||||
List.of(SVR2_MRENCLAVE));
|
||||
}
|
||||
|
||||
private LiveConfig() {
|
||||
|
||||
@ -28,20 +28,10 @@ public class ServiceConfig {
|
||||
public static final int MAXIMUM_ONE_OFF_REQUEST_SIZE = 3;
|
||||
public static final long UNREGISTERED_LIFESPAN = TimeUnit.DAYS.toMillis(30);
|
||||
|
||||
public static AccountAttributes.Capabilities getCapabilities(
|
||||
final boolean isPrimaryDevice,
|
||||
final boolean hasPhoneNumber
|
||||
) {
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
||||
final var attachmentBackfill = !isPrimaryDevice;
|
||||
final var spqr = true;
|
||||
final var usernameSyncChangeMessage = !isPrimaryDevice;
|
||||
final var optionalPhoneNumber = !isPrimaryDevice || !hasPhoneNumber;
|
||||
return new AccountAttributes.Capabilities(true,
|
||||
true,
|
||||
attachmentBackfill,
|
||||
spqr,
|
||||
usernameSyncChangeMessage,
|
||||
optionalPhoneNumber);
|
||||
return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr);
|
||||
}
|
||||
|
||||
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
||||
|
||||
@ -3,7 +3,7 @@ package org.asamk.signal.manager.config;
|
||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.network.config.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@ -3,15 +3,15 @@ package org.asamk.signal.manager.config;
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.network.config.HttpProxy;
|
||||
import org.signal.network.config.SignalCdnUrl;
|
||||
import org.signal.network.config.SignalCdsiUrl;
|
||||
import org.signal.network.config.SignalProxy;
|
||||
import org.signal.network.config.SignalServiceConfiguration;
|
||||
import org.signal.network.config.SignalServiceUrl;
|
||||
import org.signal.network.config.SignalStorageUrl;
|
||||
import org.signal.network.config.SignalSvr2Url;
|
||||
import org.signal.network.config.TrustStore;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
@ -30,8 +30,7 @@ class StagingConfig {
|
||||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
||||
.decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm");
|
||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||
private static final String SVR2_MRENCLAVE = "0ff2d7d4efbe7cfc24ac069a16fba898928dbe6c40d500c8b6da55733c727d6e";
|
||||
private static final String SVR2_MRENCLAVE_LEGACY = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302";
|
||||
private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036";
|
||||
|
||||
private static final String URL = "https://chat.staging.signal.org";
|
||||
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
||||
@ -49,10 +48,10 @@ class StagingConfig {
|
||||
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==");
|
||||
private static final byte[] genericServerPublicParams = Base64.getDecoder()
|
||||
.decode("AYhaw+NbxtNLo/RlGFEsHd904hW38LpPJ59jYJlNmT4wwtyOq4xzCs/MyXsfRbIsAYhQjDnpE0rhFtWkMcn/kV740SISwFfpPHunrtZ9h0YWz5QNNbI5I3DRGUjhKXgMU7J7s7qOr0fdms+g0e+L9FMSjJLobDkOngp/m0B5TsxTyqLscJ5VyU69Cj8txImTfHMCKrYphYfRHO78RwPoz2g2tGUAzEbKHm12OgDna2qutkE5TvYqwZczvgZyLVHdHXpvdyOlEdv4afVyWkI7u/S0XYDonIJoHlxqJoTSepZR");
|
||||
.decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N");
|
||||
|
||||
private static final byte[] backupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AXYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszS6h3nZ6vRdM/IYGK+cLynw3ucWo7idf3zjOG3b6JnGT/z7XYCr6HuOGkWH4DQWCH98hxVZMGOgmT8DCQoqebQb3oK1yrwEglRWmtI01KhRg9RGUKoQiwuej1JZEY8uaG4Uz9n1cVODJ1iuByhNqGHo+KfI4iWhjtx2AnhYqHViQ3CMd4ASGBJtic9UTFVk/4vegVIy0wfYsAmViftzK6t4");
|
||||
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
|
||||
|
||||
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
|
||||
|
||||
@ -94,7 +93,7 @@ class StagingConfig {
|
||||
createDefaultServiceConfiguration(interceptors),
|
||||
getUnidentifiedSenderTrustRoots(),
|
||||
CDSI_MRENCLAVE,
|
||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
||||
List.of(SVR2_MRENCLAVE));
|
||||
}
|
||||
|
||||
private StagingConfig() {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package org.asamk.signal.manager.config;
|
||||
|
||||
import org.signal.network.config.TrustStore;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.api.BadRequestException;
|
||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
@ -18,24 +17,19 @@ import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.signal.core.models.ServiceId.PNI;
|
||||
import org.signal.core.util.Base64;
|
||||
import org.signal.core.util.crypto.DeviceNameCipher;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.NoSessionException;
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.protocol.util.KeyHelper;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.signal.network.api.AccountApiV2.SetAccountAttributesError;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
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.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||
@ -43,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.AuthorizationFailedException;
|
||||
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.KyberPreKeyEntity;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||
@ -50,9 +46,7 @@ import org.whispersystems.signalservice.internal.push.SyncMessage;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@ -63,7 +57,6 @@ import okio.ByteString;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
|
||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||
|
||||
public class AccountHelper {
|
||||
@ -98,10 +91,10 @@ public class AccountHelper {
|
||||
} else {
|
||||
context.getPreKeyHelper().refreshPreKeysIfNecessary();
|
||||
}
|
||||
if (account.getPni() == null && account.getNumber() != null) {
|
||||
if (account.getPni() == null) {
|
||||
checkWhoAmiI();
|
||||
}
|
||||
if (!account.isPrimaryDevice() && account.getPni() != null && account.getPniIdentityKeyPair() == null) {
|
||||
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
||||
throw new IOException("Missing PNI identity key, relinking required");
|
||||
}
|
||||
if (account.getPreviousStorageVersion() < 10
|
||||
@ -140,9 +133,8 @@ public class AccountHelper {
|
||||
final var whoAmI = dependencies.getAccountManager().getWhoAmI();
|
||||
final var number = whoAmI.getNumber();
|
||||
final var aci = ACI.parseOrThrow(whoAmI.getAci());
|
||||
final var pni = whoAmI.getPni() == null ? null : PNI.parseOrThrow(whoAmI.getPni());
|
||||
if (Objects.equals(number, account.getNumber()) && aci.equals(account.getAci()) && Objects.equals(pni,
|
||||
account.getPni())) {
|
||||
final var pni = PNI.parseOrThrow(whoAmI.getPni());
|
||||
if (number.equals(account.getNumber()) && aci.equals(account.getAci()) && pni.equals(account.getPni())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -153,7 +145,7 @@ public class AccountHelper {
|
||||
account.setNumber(number);
|
||||
account.setAci(aci);
|
||||
account.setPni(pni);
|
||||
if (pni != null && account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
||||
if (account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
||||
account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
|
||||
}
|
||||
account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
||||
@ -288,7 +280,7 @@ public class AccountHelper {
|
||||
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
|
||||
pniChangeNumber);
|
||||
encryptedDeviceMessages.add(message);
|
||||
} catch (UntrustedIdentityException | IOException | InvalidKeyException | NoSessionException e) {
|
||||
} catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@ -328,14 +320,10 @@ public class AccountHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
handlePniChangeNumberMessage(selfChangeNumber, updatePni, false);
|
||||
handlePniChangeNumberMessage(selfChangeNumber, updatePni);
|
||||
}
|
||||
|
||||
public boolean handlePniChangeNumberMessage(
|
||||
final SyncMessage.PniChangeNumber pniChangeNumber,
|
||||
final PNI updatedPni,
|
||||
final boolean forcePniPreKeyRotation
|
||||
) {
|
||||
public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
|
||||
if (pniChangeNumber.identityKeyPair != null
|
||||
&& pniChangeNumber.registrationId != null
|
||||
&& pniChangeNumber.signedPreKey != null) {
|
||||
@ -349,19 +337,10 @@ public class AccountHelper {
|
||||
pniChangeNumber.lastResortKyberPreKey != null
|
||||
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
|
||||
: null);
|
||||
if (forcePniPreKeyRotation) {
|
||||
try {
|
||||
context.getPreKeyHelper().forceRefreshPreKeys(ServiceIdType.PNI);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to force refresh PNI pre keys after PNI change sync", e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to handle change number message", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final int USERNAME_MIN_LENGTH = 3;
|
||||
@ -403,17 +382,13 @@ public class AccountHelper {
|
||||
}
|
||||
|
||||
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) {
|
||||
candidateHashes.add(candidate.getHash());
|
||||
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
|
||||
}
|
||||
|
||||
final var usernameHash = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||
final var hashIndex = candidateHashes.stream()
|
||||
.filter(candidateHash -> Arrays.equals(candidateHash, usernameHash))
|
||||
.findFirst()
|
||||
.map(candidateHashes::indexOf)
|
||||
.orElse(-1);
|
||||
final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
||||
if (hashIndex == -1) {
|
||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||
throw new IOException("Unexpected username response");
|
||||
@ -506,7 +481,8 @@ public class AccountHelper {
|
||||
final var usernameLink = account.getUsernameLink();
|
||||
|
||||
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.");
|
||||
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||
account.setUsernameLink(linkComponents);
|
||||
@ -531,34 +507,28 @@ public class AccountHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void deleteUsername() throws IOException {
|
||||
handleResponseException(dependencies.getAccountApi().deleteUsernameHash());
|
||||
handleResponseException(dependencies.getAccountApi().deleteUsername());
|
||||
account.setUsernameLink(null);
|
||||
account.setUsername(null);
|
||||
logger.debug("[deleteUsername] Successfully deleted the username.");
|
||||
}
|
||||
|
||||
public void setDeviceName(String deviceName) {
|
||||
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
||||
account.setEncryptedDeviceName(Base64.encodeWithoutPadding(encryptedDeviceName));
|
||||
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
||||
account.setEncryptedDeviceName(encryptedDeviceName);
|
||||
}
|
||||
|
||||
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
||||
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
||||
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||
.setDeviceName(encryptedDeviceName, deviceId, cont));
|
||||
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
||||
handleResponseException(dependencies.getLinkDeviceApi().setDeviceName(encryptedDeviceName, 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 {
|
||||
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||
.getDevices(cont));
|
||||
final var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||
final var deviceId = account.getDeviceId();
|
||||
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
|
||||
if (device.isPresent()) {
|
||||
@ -567,20 +537,7 @@ public class AccountHelper {
|
||||
}
|
||||
|
||||
public void updateAccountAttributes() throws IOException {
|
||||
if (account.getNumber() != null) {
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
.setAccountAttributes(account.getAccountAttributes(null)));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
handleResponseExceptionSuspend(cont -> dependencies.getAccountApiV2()
|
||||
.setAccountAttributes(account.getAccountAttributesV2(), cont));
|
||||
} catch (BadRequestException e) {
|
||||
if (e.getError() instanceof SetAccountAttributesError.Unauthorized) {
|
||||
throw new AuthorizationFailedException(401, "Authorization failed!");
|
||||
}
|
||||
throw new IOException("Account attribute update rate limited; try again later");
|
||||
}
|
||||
handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
|
||||
}
|
||||
|
||||
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
|
||||
@ -605,39 +562,36 @@ public class AccountHelper {
|
||||
account.getOrCreatePinMasterKey(),
|
||||
account.getOrCreateMediaRootBackupKey(),
|
||||
verificationCode.getVerificationCode(),
|
||||
null,
|
||||
account.getAuthCredentialSalt()));
|
||||
null));
|
||||
account.setMultiDevice(true);
|
||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||
}
|
||||
|
||||
public void removeLinkedDevices(int deviceId) throws IOException {
|
||||
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi().removeDevice(deviceId, cont));
|
||||
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||
.getDevices(cont));
|
||||
handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
|
||||
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||
account.setMultiDevice(devices.size() > 1);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void migrateRegistrationPin() throws IOException {
|
||||
var masterKey = account.getOrCreatePinMasterKey();
|
||||
|
||||
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
||||
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setRegistrationPin(String pin) throws IOException {
|
||||
var masterKey = account.getOrCreatePinMasterKey();
|
||||
|
||||
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
||||
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||
|
||||
account.setRegistrationLockPin(pin);
|
||||
updateAccountAttributes();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void removeRegistrationPin() throws IOException {
|
||||
// Remove KBS Pin
|
||||
context.getPinHelper().removeRegistrationLockPin();
|
||||
@ -650,7 +604,7 @@ public class AccountHelper {
|
||||
// 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 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);
|
||||
unregisteredListener.call();
|
||||
|
||||
@ -1,25 +1,20 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||
import org.asamk.signal.manager.api.Message.AttachmentDimensions;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.storage.AttachmentStore;
|
||||
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||
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.IOException;
|
||||
@ -35,10 +30,8 @@ public class AttachmentHelper {
|
||||
|
||||
private final SignalDependencies dependencies;
|
||||
private final AttachmentStore attachmentStore;
|
||||
private final Context context;
|
||||
|
||||
public AttachmentHelper(final Context context) {
|
||||
this.context = context;
|
||||
this.dependencies = context.getDependencies();
|
||||
this.attachmentStore = context.getAttachmentStore();
|
||||
}
|
||||
@ -51,13 +44,8 @@ public class AttachmentHelper {
|
||||
return attachmentStore.retrieveAttachment(id);
|
||||
}
|
||||
|
||||
public List<SignalServiceAttachment> uploadAttachments(
|
||||
final List<String> attachments,
|
||||
final List<AttachmentDimensions> dimensions,
|
||||
final List<String> blurHashes,
|
||||
boolean voiceNote
|
||||
) throws AttachmentInvalidException, IOException {
|
||||
final var attachmentStreams = createAttachmentStreams(attachments, dimensions, blurHashes, voiceNote);
|
||||
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments, boolean voiceNote) throws AttachmentInvalidException, IOException {
|
||||
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
|
||||
|
||||
try {
|
||||
// Upload attachments here, so we only upload once even for multiple recipients
|
||||
@ -74,73 +62,24 @@ public class AttachmentHelper {
|
||||
}
|
||||
|
||||
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
|
||||
return uploadAttachments(attachments, List.of(), List.of(), false);
|
||||
return uploadAttachments(attachments, false);
|
||||
}
|
||||
|
||||
private List<SignalServiceAttachmentStream> createAttachmentStreams(
|
||||
List<String> attachments,
|
||||
List<AttachmentDimensions> dimensions,
|
||||
List<String> blurHashes,
|
||||
boolean voiceNote
|
||||
) throws AttachmentInvalidException, IOException {
|
||||
private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments, boolean voiceNote) throws AttachmentInvalidException, IOException {
|
||||
if (attachments == null) {
|
||||
return null;
|
||||
}
|
||||
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
||||
for (var i = 0; i < attachments.size(); i++) {
|
||||
final var size = i < dimensions.size() ? dimensions.get(i) : null;
|
||||
final var blurHash = i < blurHashes.size() && !blurHashes.get(i).isEmpty() ? blurHashes.get(i) : null;
|
||||
signalServiceAttachments.add(getAttachmentStream(attachments.get(i), size, blurHash, voiceNote));
|
||||
for (var attachment : attachments) {
|
||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||
signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, voiceNote, uploadSpec));
|
||||
}
|
||||
return signalServiceAttachments;
|
||||
}
|
||||
|
||||
private SignalServiceAttachmentStream getAttachmentStream(
|
||||
final String attachment,
|
||||
final AttachmentDimensions dimensions,
|
||||
final String blurHash,
|
||||
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,
|
||||
dimensions,
|
||||
blurHash,
|
||||
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 {
|
||||
final var attachmentStream = getAttachmentStream(attachment, null, null, false);
|
||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||
var attachmentStream = AttachmentUtils.createAttachmentStream(attachment, uploadSpec);
|
||||
return uploadAttachment(attachmentStream);
|
||||
}
|
||||
|
||||
|
||||
@ -105,8 +105,6 @@ public class CallManager implements AutoCloseable {
|
||||
recipientAddress,
|
||||
recipientId);
|
||||
activeCalls.put(callId, state);
|
||||
dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
|
||||
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
|
||||
fireCallEvent(state, null);
|
||||
|
||||
// Spawn call tunnel binary and connect control channel
|
||||
@ -199,6 +197,11 @@ public class CallManager implements AutoCloseable {
|
||||
if (callEventListeners.isEmpty()) {
|
||||
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
|
||||
callIdUnsigned(callId));
|
||||
|
||||
final var result = sendBusyMessage(callId, recipientId, deviceId);
|
||||
if (!result.isSuccess()) {
|
||||
logger.warn("Failed to send busy for unhandled call {}", callIdUnsigned(callId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -649,7 +652,7 @@ public class CallManager implements AutoCloseable {
|
||||
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
|
||||
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);
|
||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||
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) {
|
||||
var state = activeCalls.remove(callId);
|
||||
dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
|
||||
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
|
||||
if (state == null) return;
|
||||
|
||||
state.state = CallInfo.State.ENDED;
|
||||
@ -711,7 +712,7 @@ public class CallManager implements AutoCloseable {
|
||||
&& !"rejected".equals(reason)
|
||||
&& !"remote_busy".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);
|
||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||
if (!result.isSuccess()) {
|
||||
|
||||
@ -85,18 +85,12 @@ public class ContactHelper {
|
||||
}
|
||||
|
||||
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);
|
||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||
if (blocked) {
|
||||
builder.withIsProfileSharingEnabled(false);
|
||||
}
|
||||
account.getContactStore()
|
||||
.storeContact(recipientId,
|
||||
builder.withIsBlocked(blocked).withBlockedAt(blocked ? blockedAt : 0).build());
|
||||
account.getContactStore().storeContact(recipientId, builder.withIsBlocked(blocked).build());
|
||||
}
|
||||
|
||||
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
||||
|
||||
@ -123,7 +123,7 @@ public class GroupHelper {
|
||||
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));
|
||||
}
|
||||
|
||||
@ -363,28 +363,6 @@ public class GroupHelper {
|
||||
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 {
|
||||
var group = getGroupForUpdating(groupId);
|
||||
|
||||
@ -459,21 +437,12 @@ public class GroupHelper {
|
||||
}
|
||||
|
||||
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);
|
||||
if (group == null) {
|
||||
throw new GroupNotFoundException(groupId);
|
||||
}
|
||||
|
||||
group.setBlocked(blocked);
|
||||
group.setBlockedAt(blocked ? blockedAt : 0);
|
||||
account.getGroupStore().updateGroup(group);
|
||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||
}
|
||||
@ -589,24 +558,16 @@ public class GroupHelper {
|
||||
private void storeProfileKeysFromMembers(final DecryptedGroup group) {
|
||||
for (var member : group.members) {
|
||||
final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
|
||||
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
|
||||
}
|
||||
for (var member : group.requestingMembers) {
|
||||
final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
|
||||
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
private void storeProfileKeyIfMissing(final ServiceId serviceId, final byte[] profileKeyBytes) {
|
||||
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
|
||||
final var profileStore = account.getProfileStore();
|
||||
if (profileStore.getProfileKey(recipientId) != null) {
|
||||
// We already have a profile key, not updating it from a non-authoritative source
|
||||
return;
|
||||
}
|
||||
try {
|
||||
profileStore.storeProfileKey(recipientId, new ProfileKey(profileKeyBytes));
|
||||
} catch (InvalidInputException ignored) {
|
||||
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
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray()));
|
||||
} catch (InvalidInputException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -765,7 +726,7 @@ public class GroupHelper {
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
final var newMembers = new HashSet<>(members);
|
||||
newMembers.removeAll(group.getMemberRecipientIds());
|
||||
newMembers.removeAll(group.getMembers());
|
||||
newMembers.removeAll(group.getRequestingMembers());
|
||||
if (!newMembers.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
|
||||
@ -807,8 +768,12 @@ public class GroupHelper {
|
||||
newAdmins.retainAll(group.getMemberRecipientIds());
|
||||
newAdmins.removeAll(group.getAdminMemberRecipientIds());
|
||||
if (!newAdmins.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, newAdmins, true);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
for (var admin : newAdmins) {
|
||||
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);
|
||||
existingRemoveAdmins.retainAll(group.getAdminMemberRecipientIds());
|
||||
if (!existingRemoveAdmins.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, existingRemoveAdmins, false);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
for (var admin : existingRemoveAdmins) {
|
||||
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.UuidCiphertext;
|
||||
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.GroupChange;
|
||||
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.NotAbleToApplyGroupV2ChangeException;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -136,7 +136,7 @@ class GroupV2Helper {
|
||||
|
||||
int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup) {
|
||||
ByteString aciBytes = getSelfAci().toByteString();
|
||||
ByteString pniBytes = getSelfPni() == null ? null : getSelfPni().toByteString();
|
||||
ByteString pniBytes = getSelfPni().toByteString();
|
||||
for (DecryptedMember decryptedMember : partialDecryptedGroup.members) {
|
||||
if (decryptedMember.aciBytes.equals(aciBytes) || decryptedMember.pniBytes.equals(pniBytes)) {
|
||||
return decryptedMember.joinedAtRevision;
|
||||
@ -264,9 +264,6 @@ class GroupV2Helper {
|
||||
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
|
||||
final var selfAci = getSelfAci();
|
||||
var selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, selfAci);
|
||||
if (selfPendingMember.isEmpty() && getSelfPni() != null) {
|
||||
selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, getSelfPni());
|
||||
}
|
||||
|
||||
if (selfPendingMember.isPresent()) {
|
||||
return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
|
||||
@ -504,25 +501,18 @@ class GroupV2Helper {
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> recipientIds,
|
||||
RecipientId recipientId,
|
||||
boolean admin
|
||||
) throws IOException {
|
||||
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 change = new GroupChange.Actions.Builder();
|
||||
final var memberRoles = recipientIds.stream()
|
||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||
.map(SignalServiceAddress::getServiceId)
|
||||
.filter(m -> m instanceof ACI)
|
||||
.map(m -> (ACI) m)
|
||||
.map(aci -> new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(groupOperations.encryptServiceId(
|
||||
aci)).role(newRole).build())
|
||||
.toList();
|
||||
if (memberRoles.size() < recipientIds.size()) {
|
||||
if (address.getServiceId() instanceof ACI aci) {
|
||||
final var change = groupOperations.createChangeMemberRole(aci, newRole);
|
||||
return commitChange(groupInfoV2, change);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can't make a PNI a group admin.");
|
||||
}
|
||||
change.modifyMemberRoles(memberRoles);
|
||||
return commitChange(groupInfoV2, change);
|
||||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setMessageExpirationTimer(
|
||||
@ -543,14 +533,6 @@ class GroupV2Helper {
|
||||
return commitChange(groupInfoV2, change);
|
||||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> terminateGroup(
|
||||
GroupInfoV2 groupInfoV2
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
final var change = groupOperations.createTerminateGroup();
|
||||
return commitChange(groupInfoV2, change);
|
||||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setMemberLabels(
|
||||
GroupInfoV2 groupInfoV2,
|
||||
String labelEmoji,
|
||||
@ -758,14 +740,8 @@ class GroupV2Helper {
|
||||
var authCredentialResponse = groupApiCredentials.get(todaySeconds);
|
||||
final var aci = getSelfAci();
|
||||
final var pni = getSelfPni();
|
||||
final var authCredentialSalt = context.getAccount().getAuthCredentialSalt();
|
||||
return dependencies.getGroupsV2Api()
|
||||
.getGroupsV2AuthorizationString(aci,
|
||||
pni,
|
||||
authCredentialSalt,
|
||||
todaySeconds,
|
||||
groupSecretParams,
|
||||
authCredentialResponse);
|
||||
.getGroupsV2AuthorizationString(aci, pni, todaySeconds, groupSecretParams, authCredentialResponse);
|
||||
}
|
||||
|
||||
private ACI getSelfAci() {
|
||||
|
||||
@ -109,8 +109,8 @@ public final class IncomingMessageHandler {
|
||||
SignalServiceContent content = null;
|
||||
if (!envelope.isReceipt()) {
|
||||
account.getIdentityKeyStore().setRetryingDecryption(true);
|
||||
final var destination = getDestination(envelope).serviceId();
|
||||
try {
|
||||
final var destination = getDestination(envelope).serviceId();
|
||||
final var cipherResult = dependencies.getCipher(destination == null
|
||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||
@ -140,30 +140,15 @@ public final class IncomingMessageHandler {
|
||||
final Manager.ReceiveMessageHandler handler
|
||||
) {
|
||||
final var actions = new ArrayList<HandleAction>();
|
||||
if (envelope.isPreKeySignalMessage()) {
|
||||
actions.add(RefreshPreKeysAction.create());
|
||||
}
|
||||
SignalServiceContent content = null;
|
||||
Exception exception = null;
|
||||
if (envelope.getSourceServiceId() != null) {
|
||||
// Store uuid if we don't have it already
|
||||
// uuid in envelope is sent by server
|
||||
account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId());
|
||||
}
|
||||
envelope.getSourceServiceId().map(ServiceId::parseOrNull)
|
||||
// Store uuid if we don't have it already
|
||||
// uuid in envelope is sent by server
|
||||
.ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId));
|
||||
if (!envelope.isReceipt()) {
|
||||
final var destination = getDestination(envelope).serviceId();
|
||||
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
|
||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||
@ -188,13 +173,7 @@ public final class IncomingMessageHandler {
|
||||
logger.debug("Received invalid message from blocked contact, ignoring.");
|
||||
} else {
|
||||
var serviceId = ServiceId.parseOrNull(e.getSender());
|
||||
ServiceId destination;
|
||||
try {
|
||||
destination = getDestination(envelope).serviceId();
|
||||
} catch (InvalidMessageException ex) {
|
||||
destination = null;
|
||||
}
|
||||
if (serviceId != null && destination != null) {
|
||||
if (serviceId != null) {
|
||||
final var isSelf = sender.equals(account.getSelfRecipientId())
|
||||
&& e.getSenderDevice() == account.getDeviceId();
|
||||
logger.debug("Received invalid message, queuing renew session action.");
|
||||
@ -332,17 +311,12 @@ public final class IncomingMessageHandler {
|
||||
final var sender = senderDeviceAddress.recipientId();
|
||||
final var senderServiceId = senderDeviceAddress.serviceId();
|
||||
final var senderDeviceId = senderDeviceAddress.deviceId();
|
||||
final DeviceAddress destination;
|
||||
try {
|
||||
destination = getDestination(envelope);
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
final var destination = getDestination(envelope);
|
||||
|
||||
if (destination.serviceId.equals(account.getPni())) {
|
||||
account.getRecipientStore().markNeedsPniSignature(sender, true);
|
||||
if (account.getPni().equals(destination.serviceId)) {
|
||||
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true);
|
||||
} else if (account.getAci().equals(destination.serviceId)) {
|
||||
account.getRecipientStore().markNeedsPniSignature(sender, false);
|
||||
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, false);
|
||||
}
|
||||
|
||||
if (content.getReceiptMessage().isPresent()) {
|
||||
@ -498,11 +472,10 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
|
||||
logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
|
||||
final var recipientId = account.getRecipientTrustedResolver()
|
||||
account.getRecipientTrustedResolver()
|
||||
.resolveRecipientTrusted(Optional.of(ACI.from(aci.getRawUuid())),
|
||||
Optional.of(pni),
|
||||
senderAddress.getNumber());
|
||||
account.getRecipientStore().markPniSignatureVerified(recipientId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -626,12 +599,13 @@ public final class IncomingMessageHandler {
|
||||
for (var individual : blockedListMessage.individuals) {
|
||||
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
||||
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
||||
context.getContactHelper().setContactBlocked(recipientId, true, individual.getBlockedAt());
|
||||
context.getContactHelper().setContactBlocked(recipientId, true);
|
||||
}
|
||||
for (var group : blockedListMessage.groups) {
|
||||
final var groupId = GroupId.unknownVersion(group.getGroupId());
|
||||
for (var groupId : blockedListMessage.groupIds.stream()
|
||||
.map(GroupId::unknownVersion)
|
||||
.collect(Collectors.toSet())) {
|
||||
try {
|
||||
context.getGroupHelper().setGroupBlocked(groupId, true, group.getBlockedAt());
|
||||
context.getGroupHelper().setGroupBlocked(groupId, true);
|
||||
} catch (GroupNotFoundException e) {
|
||||
logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
|
||||
groupId.toBase64());
|
||||
@ -717,30 +691,11 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
if (syncMessage.getPniChangeNumber().isPresent()) {
|
||||
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
||||
if (account.isPrimaryDevice()) {
|
||||
logger.warn("Received PNI change number sync message on primary device, ignoring.");
|
||||
} else if (sender.deviceId() != SignalServiceAddress.DEFAULT_DEVICE_ID) {
|
||||
logger.warn("Received PNI change number sync message from non-primary device {}, ignoring.",
|
||||
sender.deviceId());
|
||||
} else {
|
||||
final var envelopeServerTimestamp = envelope.getServerDeliveredTimestamp();
|
||||
final var lastAppliedServerTimestamp = account.getLastAppliedPniChangeServerTimestamp();
|
||||
if (envelopeServerTimestamp <= lastAppliedServerTimestamp) {
|
||||
logger.warn(
|
||||
"PNI change number sync server timestamp ({}) is not newer than last applied ({}), treating as replay.",
|
||||
envelopeServerTimestamp,
|
||||
lastAppliedServerTimestamp);
|
||||
} else {
|
||||
final var updatedPniString = envelope.getUpdatedPni();
|
||||
if (updatedPniString != null && !updatedPniString.isEmpty()) {
|
||||
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
|
||||
final var applied = context.getAccountHelper()
|
||||
.handlePniChangeNumberMessage(pniChangeNumber, updatedPni, true);
|
||||
if (applied) {
|
||||
account.setLastAppliedPniChangeServerTimestamp(envelopeServerTimestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("Received PNI change number sync message, applying.");
|
||||
final var updatedPniString = envelope.getUpdatedPni();
|
||||
if (updatedPniString != null && !updatedPniString.isEmpty()) {
|
||||
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
|
||||
context.getAccountHelper().handlePniChangeNumberMessage(pniChangeNumber, updatedPni);
|
||||
}
|
||||
}
|
||||
if (syncMessage.getDeviceNameChange().isPresent()) {
|
||||
@ -836,15 +791,6 @@ public final class IncomingMessageHandler {
|
||||
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)) {
|
||||
return message == null
|
||||
|| message.getBody().isPresent()
|
||||
@ -928,6 +874,11 @@ public final class IncomingMessageHandler {
|
||||
|
||||
final var selfAddress = isSync ? source : destination;
|
||||
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.getGroupContext().isPresent()) {
|
||||
final var groupContext = message.getGroupContext().get();
|
||||
@ -1096,7 +1047,7 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
|
||||
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) {
|
||||
return new SignalServiceAddress(serviceId);
|
||||
} else if (content != null) {
|
||||
@ -1107,7 +1058,7 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
|
||||
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) {
|
||||
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(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();
|
||||
if (destination == null || destination.isUnknown()) {
|
||||
throw new InvalidMessageException("Missing destination");
|
||||
}
|
||||
if (!destination.equals(account.getAci()) && !destination.equals(account.getPni())) {
|
||||
throw new InvalidMessageException("Message not intended for this account");
|
||||
return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
|
||||
}
|
||||
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(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.PreKeyRecord;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@ -84,8 +84,7 @@ public class PreKeyHelper {
|
||||
) throws IOException {
|
||||
OneTimePreKeyCounts preKeyCounts;
|
||||
try {
|
||||
preKeyCounts = handleResponseException(dependencies.getKeysApi()
|
||||
.getAvailablePreKeyCountsSync(serviceIdType));
|
||||
preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
|
||||
} catch (AuthorizationFailedException e) {
|
||||
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
|
||||
preKeyCounts = new OneTimePreKeyCounts(0, 0);
|
||||
@ -146,7 +145,7 @@ public class PreKeyHelper {
|
||||
kyberPreKeyRecords);
|
||||
var needsReset = false;
|
||||
try {
|
||||
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeysSync(preKeyUpload));
|
||||
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
|
||||
try {
|
||||
if (preKeyRecords != null) {
|
||||
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.Utils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.signal.core.util.ExpiringProfileCredentialUtil;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.network.exceptions.PushNetworkException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.push.SignalServiceAddress;
|
||||
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.util.ExpiringProfileCredentialUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
@ -103,16 +103,8 @@ public final class ProfileHelper {
|
||||
return getRecipientProfiles(recipientIds, false);
|
||||
}
|
||||
|
||||
public boolean refreshRecipientProfile(RecipientId recipientId) {
|
||||
try {
|
||||
blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE, false));
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to retrieve profile for {}, ignoring: {}",
|
||||
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
||||
e.getMessage());
|
||||
return false;
|
||||
}
|
||||
public void refreshRecipientProfile(RecipientId recipientId) {
|
||||
getRecipientProfile(recipientId, true);
|
||||
}
|
||||
|
||||
public void refreshRecipientProfiles(Collection<RecipientId> recipientIds) {
|
||||
@ -142,9 +134,7 @@ public final class ProfileHelper {
|
||||
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL,
|
||||
false));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to retrieve profile key credential for {}, ignoring: {}",
|
||||
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
||||
e.getMessage());
|
||||
logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -271,9 +261,7 @@ public final class ProfileHelper {
|
||||
try {
|
||||
blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE, false));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to retrieve profile for {}, ignoring: {}",
|
||||
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
||||
e.getMessage());
|
||||
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return account.getProfileStore().getProfile(recipientId);
|
||||
@ -391,9 +379,7 @@ public final class ProfileHelper {
|
||||
|
||||
logger.trace("Done handling retrieved profile");
|
||||
}).doOnError(e -> {
|
||||
logger.warn("Failed to retrieve profile for {}, ignoring: {}",
|
||||
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
|
||||
e.getMessage());
|
||||
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
||||
final var profile = account.getProfileStore().getProfile(recipientId);
|
||||
final var newProfile = (
|
||||
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.messageCache.CachedMessage;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.signal.core.models.ServiceId;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.messages.EnvelopeResponse;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
@ -148,19 +148,15 @@ public class ReceiveHelper {
|
||||
logger.debug("Retrieved {} envelopes!", batch.size());
|
||||
isWaitingForMessage = false;
|
||||
for (final var it : batch) {
|
||||
if (it instanceof EnvelopeResponse.Unparseable) {
|
||||
logger.warn("Received unparseable envelope from server, ignoring.");
|
||||
} else if (it instanceof EnvelopeResponse.Parsed parsed) {
|
||||
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(parsed.getEnvelope(),
|
||||
parsed.getServerDeliveredTimestamp());
|
||||
final var sourceServiceId = envelope1.getSourceServiceId();
|
||||
final var recipientId = sourceServiceId == null
|
||||
? null
|
||||
: account.getRecipientResolver().resolveRecipient(sourceServiceId);
|
||||
logger.trace("Storing new message from {}", recipientId);
|
||||
// store message on disk, before acknowledging receipt to the server
|
||||
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
||||
}
|
||||
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
|
||||
it.getServerDeliveredTimestamp());
|
||||
final var recipientId = envelope1.getSourceServiceId()
|
||||
.map(ServiceId::parseOrNull)
|
||||
.map(s -> account.getRecipientResolver().resolveRecipient(s))
|
||||
.orElse(null);
|
||||
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 {
|
||||
signalWebSocket.sendAck(it);
|
||||
} catch (IOException e) {
|
||||
@ -172,9 +168,6 @@ public class ReceiveHelper {
|
||||
backOffCounter = 0;
|
||||
|
||||
if (queueNotEmpty) {
|
||||
if (cachedMessage[0] == null) {
|
||||
continue;
|
||||
}
|
||||
if (remainingMessages > 0) {
|
||||
remainingMessages -= 1;
|
||||
}
|
||||
@ -245,7 +238,7 @@ public class ReceiveHelper {
|
||||
if (exception instanceof UntrustedIdentityException) {
|
||||
logger.debug("Keeping message with untrusted identity in message cache");
|
||||
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()
|
||||
.resolveRecipient(ACI.parseOrThrow(address.aci().get()));
|
||||
try {
|
||||
@ -299,7 +292,7 @@ public class ReceiveHelper {
|
||||
cachedMessage.delete();
|
||||
return null;
|
||||
}
|
||||
if (envelope.getSourceServiceId() == null) {
|
||||
if (envelope.getSourceServiceId().isEmpty()) {
|
||||
final var identifier = ((UntrustedIdentityException) exception).getSender();
|
||||
final var recipientId = account.getRecipientResolver()
|
||||
.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.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
import java.io.IOException;
|
||||
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.SignalServiceEditMessage;
|
||||
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.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
@ -332,91 +330,6 @@ public class SendHelper {
|
||||
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(
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final GroupInfo g,
|
||||
@ -567,7 +480,7 @@ public class SendHelper {
|
||||
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);
|
||||
if (g == null) {
|
||||
throw new GroupNotFoundException(groupId);
|
||||
@ -575,10 +488,6 @@ public class SendHelper {
|
||||
if (!g.isMember(account.getSelfRecipientId())) {
|
||||
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()) {
|
||||
g.setProfileSharingEnabled(true);
|
||||
account.getGroupStore().updateGroup(g);
|
||||
@ -598,31 +507,9 @@ public class SendHelper {
|
||||
) throws IOException {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// Recipients that are already known to be unregistered are skipped here.
|
||||
// Otherwise every group send re-attempts them via the slow legacy 1:1
|
||||
// fan-out, which on large groups can take tens of seconds (and time out).
|
||||
// The unregistered flag is maintained independently by profile/CDS
|
||||
// discovery, which clears it once a recipient registers again, so they
|
||||
// are re-included automatically. An unregisteredFailure result is still
|
||||
// returned for each skipped recipient, so callers see them unchanged.
|
||||
final var skippedResults = new ArrayList<SendMessageResult>();
|
||||
final Set<RecipientId> targetRecipientIds;
|
||||
final var unregisteredRecipientIds = account.getRecipientStore().getUnregisteredRecipientIds(recipientIds);
|
||||
if (unregisteredRecipientIds.isEmpty()) {
|
||||
targetRecipientIds = recipientIds;
|
||||
} else {
|
||||
logger.debug("Skipping {} known-unregistered recipient(s) in group send.", unregisteredRecipientIds.size());
|
||||
targetRecipientIds = new HashSet<>(recipientIds);
|
||||
targetRecipientIds.removeAll(unregisteredRecipientIds);
|
||||
for (final var recipientId : unregisteredRecipientIds) {
|
||||
skippedResults.add(SendMessageResult.unregisteredFailure(context.getRecipientHelper()
|
||||
.resolveSignalServiceAddress(recipientId)));
|
||||
}
|
||||
}
|
||||
|
||||
final var addressesMap = targetRecipientIds.stream()
|
||||
final var addressesMap = recipientIds.stream()
|
||||
.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 groupSecretParams = groupInfo instanceof GroupInfoV2 gv2
|
||||
? GroupSecretParams.deriveFromMasterKey((gv2.getMasterKey()))
|
||||
@ -636,7 +523,7 @@ public class SendHelper {
|
||||
: groupSendEndorsementsResult.first();
|
||||
Set<RecipientId> senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null
|
||||
? Set.of()
|
||||
: targetRecipientIds.stream()
|
||||
: recipientIds.stream()
|
||||
.filter(s -> this.isSenderKeyCapable(s,
|
||||
addressesMap.get(s),
|
||||
unidentifiedAccessesMap.get(s),
|
||||
@ -646,9 +533,7 @@ public class SendHelper {
|
||||
logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
|
||||
senderKeyTargets = Set.of();
|
||||
} else {
|
||||
logger.debug("Can use sender key for {}/{} recipients.",
|
||||
senderKeyTargets.size(),
|
||||
targetRecipientIds.size());
|
||||
logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), 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);
|
||||
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()) {
|
||||
logger.debug("Need to do {} legacy sends.", legacyTargets.size());
|
||||
} else {
|
||||
@ -720,7 +605,6 @@ public class SendHelper {
|
||||
isRecipientUpdate || !allResults.isEmpty());
|
||||
allResults.addAll(results);
|
||||
}
|
||||
allResults.addAll(skippedResults);
|
||||
final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
|
||||
logger.debug("Sending took {}", duration.toString());
|
||||
return allResults;
|
||||
@ -783,7 +667,7 @@ public class SendHelper {
|
||||
final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
|
||||
logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, addresses.size());
|
||||
return results;
|
||||
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException | NoSessionException e) {
|
||||
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@ -886,8 +770,7 @@ public class SendHelper {
|
||||
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
try {
|
||||
final boolean includePniSignature = account.getPni() != null && account.getRecipientStore()
|
||||
.needsPniSignature(recipientId);
|
||||
final boolean includePniSignature = account.getRecipientStore().needsPniSignature(recipientId);
|
||||
try {
|
||||
return s.send(messageSender,
|
||||
address,
|
||||
@ -976,7 +859,7 @@ public class SendHelper {
|
||||
SignalServiceAddress address,
|
||||
SealedSenderAccess unidentifiedAccess,
|
||||
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 {
|
||||
@ -996,6 +879,6 @@ public class SendHelper {
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<SealedSenderAccess> unidentifiedAccess,
|
||||
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.GroupIdV2;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
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.storage.SignalAccount;
|
||||
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.ContactRecordProcessor;
|
||||
import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor;
|
||||
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.StorageSyncValidations;
|
||||
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.util.SetUtil;
|
||||
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.LoggerFactory;
|
||||
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.StorageId;
|
||||
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.StorageRecord;
|
||||
|
||||
@ -43,10 +38,8 @@ import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
@ -57,19 +50,16 @@ public class StorageHelper {
|
||||
private static final List<Integer> KNOWN_TYPES = List.of(ManifestRecord.Identifier.Type.CONTACT.getValue(),
|
||||
ManifestRecord.Identifier.Type.GROUPV1.getValue(),
|
||||
ManifestRecord.Identifier.Type.GROUPV2.getValue(),
|
||||
ManifestRecord.Identifier.Type.ACCOUNT.getValue(),
|
||||
ManifestRecord.Identifier.Type.STICKER_PACK.getValue());
|
||||
ManifestRecord.Identifier.Type.ACCOUNT.getValue());
|
||||
|
||||
private final SignalAccount account;
|
||||
private final SignalDependencies dependencies;
|
||||
private final Context context;
|
||||
private final StorageSyncLoopDetector storageSyncLoopDetector;
|
||||
|
||||
public StorageHelper(final Context context) {
|
||||
this.account = context.getAccount();
|
||||
this.dependencies = context.getDependencies();
|
||||
this.context = context;
|
||||
this.storageSyncLoopDetector = new StorageSyncLoopDetector(account::isMultiDevice);
|
||||
}
|
||||
|
||||
public void syncDataWithStorage() throws IOException {
|
||||
@ -88,8 +78,6 @@ public class StorageHelper {
|
||||
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
||||
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
||||
localManifestVersion);
|
||||
final var fetchedRemoteManifest = result instanceof ManifestIfDifferentVersionResult.DifferentVersion;
|
||||
final Set<StorageId> identityConflictsPendingRepair = new HashSet<>();
|
||||
|
||||
var needsForcePush = false;
|
||||
final var remoteManifest = switch (result) {
|
||||
@ -118,22 +106,18 @@ public class StorageHelper {
|
||||
|
||||
if (remoteManifest.version > localManifestVersion) {
|
||||
logger.trace("Remote version was newer, reading records.");
|
||||
needsForcePush = readDataFromStorage(storageKey,
|
||||
localManifest,
|
||||
remoteManifest,
|
||||
identityConflictsPendingRepair);
|
||||
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest);
|
||||
} else if (remoteManifest.version < localManifest.version) {
|
||||
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
|
||||
}
|
||||
logger.trace("Done reading data from remote storage");
|
||||
|
||||
readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest, identityConflictsPendingRepair);
|
||||
readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest);
|
||||
}
|
||||
|
||||
logger.trace("Adding missing storageIds to local data");
|
||||
account.getRecipientStore().setMissingStorageIds();
|
||||
account.getGroupStore().setMissingStorageIds();
|
||||
account.getStickerStore().setMissingStorageIds();
|
||||
|
||||
var needsMultiDeviceSync = false;
|
||||
|
||||
@ -155,11 +139,7 @@ public class StorageHelper {
|
||||
needsForcePush = true;
|
||||
} else {
|
||||
try {
|
||||
needsMultiDeviceSync = writeToStorage(storageKey,
|
||||
remoteManifest,
|
||||
needsForcePush,
|
||||
fetchedRemoteManifest,
|
||||
identityConflictsPendingRepair);
|
||||
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
||||
} catch (RetryLaterException e) {
|
||||
// TODO retry later
|
||||
return;
|
||||
@ -204,8 +184,7 @@ public class StorageHelper {
|
||||
private boolean readDataFromStorage(
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest localManifest,
|
||||
final SignalStorageManifest remoteManifest,
|
||||
final Set<StorageId> identityConflictsPendingRepair
|
||||
final SignalStorageManifest remoteManifest
|
||||
) throws IOException {
|
||||
var needsForcePush = false;
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
@ -232,28 +211,20 @@ public class StorageHelper {
|
||||
remoteOnlyRecords.size());
|
||||
}
|
||||
|
||||
final var listListPair = processKnownRecords(connection,
|
||||
remoteOnlyRecords,
|
||||
identityConflictsPendingRepair);
|
||||
final var unknownInserts = listListPair.first();
|
||||
final var updatedStorageIds = listListPair.second();
|
||||
final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds());
|
||||
updatedStorageIds.forEach(oldUnregisteredLocalOnlyIds::remove);
|
||||
if (!idDifference.localOnlyIds().isEmpty()) {
|
||||
final var updated = account.getRecipientStore()
|
||||
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
||||
oldUnregisteredLocalOnlyIds);
|
||||
final var updatedStickers = account.getStickerStore()
|
||||
.removeStorageIdsFromLocalOnlyDeletedStickerPacks(connection, oldUnregisteredLocalOnlyIds);
|
||||
|
||||
if (updated > 0 || updatedStickers > 0) {
|
||||
logger.warn(
|
||||
"Found {} recipients and {} sticker packs that were deleted remotely but only marked deleted locally. Removed those from local store.",
|
||||
updated,
|
||||
updatedStickers);
|
||||
}
|
||||
}
|
||||
|
||||
// This logic is wrong, records should only be deleted if they're deleted remotely, not if the remote record is updated
|
||||
// if (!idDifference.localOnlyIds().isEmpty()) {
|
||||
// final var updated = account.getRecipientStore()
|
||||
// .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
||||
// idDifference.localOnlyIds());
|
||||
//
|
||||
// if (updated > 0) {
|
||||
// logger.warn(
|
||||
// "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
|
||||
// updated);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
|
||||
final var unknownDeletes = idDifference.localOnlyIds()
|
||||
.stream()
|
||||
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
|
||||
@ -277,8 +248,7 @@ public class StorageHelper {
|
||||
|
||||
private void readRecordsWithPreviouslyUnknownTypes(
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest remoteManifest,
|
||||
final Set<StorageId> identityConflictsPendingRepair
|
||||
final SignalStorageManifest remoteManifest
|
||||
) throws IOException {
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
@ -292,8 +262,9 @@ public class StorageHelper {
|
||||
|
||||
logger.debug("Found {} of the known-unknowns remotely.", remote.size());
|
||||
|
||||
processKnownRecords(connection, remote, identityConflictsPendingRepair);
|
||||
account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, knownUnknownIds);
|
||||
processKnownRecords(connection, remote);
|
||||
account.getUnknownStorageIdStore()
|
||||
.deleteUnknownStorageIds(connection, remote.stream().map(SignalStorageRecord::getId).toList());
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
@ -304,9 +275,7 @@ public class StorageHelper {
|
||||
private boolean writeToStorage(
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest remoteManifest,
|
||||
final boolean needsForcePush,
|
||||
final boolean fetchedRemoteManifest,
|
||||
final Set<StorageId> identityConflictsPendingRepair
|
||||
final boolean needsForcePush
|
||||
) throws IOException, RetryLaterException {
|
||||
final WriteOperationResult remoteWriteOperation;
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
@ -316,8 +285,10 @@ public class StorageHelper {
|
||||
var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
|
||||
logger.debug("ID Difference :: {}", idDifference);
|
||||
|
||||
final var unknownStorageIds = account.getUnknownStorageIdStore().getUnknownStorageIds(connection);
|
||||
final var unknownOnlyLocal = findUnknownOnlyLocalStorageIds(idDifference.localOnlyIds(), unknownStorageIds);
|
||||
final var unknownOnlyLocal = idDifference.localOnlyIds()
|
||||
.stream()
|
||||
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
|
||||
.toList();
|
||||
|
||||
if (!unknownOnlyLocal.isEmpty()) {
|
||||
logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size());
|
||||
@ -342,28 +313,6 @@ public class StorageHelper {
|
||||
|
||||
if (remoteWriteOperation.isEmpty()) {
|
||||
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
||||
storageSyncLoopDetector.onConverged();
|
||||
return false;
|
||||
}
|
||||
|
||||
final var onlyIdentityConflictsPendingRepair = containsOnlyIdentityConflictsPendingRepair(remoteWriteOperation,
|
||||
identityConflictsPendingRepair);
|
||||
if (onlyIdentityConflictsPendingRepair) {
|
||||
logger.warn(
|
||||
"Deferring remote write until the profile fetch says whose identity key is correct. WriteOperationResult :: {}",
|
||||
remoteWriteOperation);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -382,18 +331,11 @@ public class StorageHelper {
|
||||
remoteWriteOperation.deletes());
|
||||
switch (result) {
|
||||
case WriteStorageRecordsResult.ConflictError ignored -> {
|
||||
storageSyncLoopDetector.onWriteFailed();
|
||||
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
case WriteStorageRecordsResult.NetworkError networkError -> {
|
||||
storageSyncLoopDetector.onWriteFailed();
|
||||
throw networkError.getException();
|
||||
}
|
||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> {
|
||||
storageSyncLoopDetector.onWriteFailed();
|
||||
throw statusCodeError.getException();
|
||||
}
|
||||
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
||||
case WriteStorageRecordsResult.Success ignored -> {
|
||||
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
||||
storeManifestLocally(remoteWriteOperation.manifest());
|
||||
@ -403,22 +345,6 @@ public class StorageHelper {
|
||||
}
|
||||
}
|
||||
|
||||
static List<StorageId> findUnknownOnlyLocalStorageIds(
|
||||
final List<StorageId> localOnlyStorageIds,
|
||||
final Set<StorageId> unknownStorageIds
|
||||
) {
|
||||
return localOnlyStorageIds.stream().filter(unknownStorageIds::contains).toList();
|
||||
}
|
||||
|
||||
static boolean containsOnlyIdentityConflictsPendingRepair(
|
||||
final WriteOperationResult writeOperation,
|
||||
final Set<StorageId> identityConflictsPendingRepair
|
||||
) {
|
||||
return writeOperation.deletes().isEmpty() && !writeOperation.inserts().isEmpty() && writeOperation.inserts()
|
||||
.stream()
|
||||
.allMatch(record -> identityConflictsPendingRepair.contains(record.getId()));
|
||||
}
|
||||
|
||||
private void forcePushToStorage(
|
||||
final StorageKey storageServiceKey
|
||||
) throws IOException, RetryLaterException {
|
||||
@ -435,7 +361,6 @@ public class StorageHelper {
|
||||
final Map<RecipientId, StorageId> newContactStorageIds;
|
||||
final Map<GroupIdV1, StorageId> newGroupV1StorageIds;
|
||||
final Map<GroupIdV2, StorageId> newGroupV2StorageIds;
|
||||
final Map<StickerPackId, StorageId> newStickerPackStorageIds;
|
||||
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
@ -482,19 +407,6 @@ public class StorageHelper {
|
||||
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();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed to sync remote storage", e);
|
||||
@ -545,7 +457,6 @@ public class StorageHelper {
|
||||
connection.setAutoCommit(false);
|
||||
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
|
||||
account.getGroupStore().updateStorageIds(connection, newGroupV1StorageIds, newGroupV2StorageIds);
|
||||
account.getStickerStore().updateStorageIds(connection, newStickerPackStorageIds);
|
||||
|
||||
// delete all unknown storage ids
|
||||
account.getUnknownStorageIdStore().deleteAllUnknownStorageIds(connection);
|
||||
@ -569,21 +480,13 @@ public class StorageHelper {
|
||||
private Map<GroupIdV1, StorageId> generateGroupV1StorageIds(List<GroupIdV1> groupIds) {
|
||||
return groupIds.stream()
|
||||
.collect(Collectors.toMap(recipientId -> recipientId,
|
||||
_ -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
|
||||
recipientId -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
|
||||
}
|
||||
|
||||
private Map<GroupIdV2, StorageId> generateGroupV2StorageIds(List<GroupIdV2> groupIds) {
|
||||
return groupIds.stream()
|
||||
.collect(Collectors.toMap(recipientId -> 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())));
|
||||
recipientId -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
||||
}
|
||||
|
||||
private void storeManifestLocally(
|
||||
@ -601,7 +504,7 @@ public class StorageHelper {
|
||||
final var result = dependencies.getStorageServiceRepository()
|
||||
.readStorageRecords(storageKey, manifest.recordIkm, storageIds);
|
||||
return switch (result) {
|
||||
case StorageServiceService.StorageRecordResult.DecryptionError decryptionError -> {
|
||||
case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
|
||||
if (decryptionError.getException() instanceof InvalidKeyException) {
|
||||
logger.warn("Failed to read storage records, ignoring.");
|
||||
yield List.of();
|
||||
@ -611,11 +514,11 @@ public class StorageHelper {
|
||||
throw new IOException(decryptionError.getException());
|
||||
}
|
||||
}
|
||||
case StorageServiceService.StorageRecordResult.NetworkError networkError ->
|
||||
case StorageServiceRepository.StorageRecordResult.NetworkError networkError ->
|
||||
throw networkError.getException();
|
||||
case StorageServiceService.StorageRecordResult.StatusCodeError statusCodeError ->
|
||||
case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError ->
|
||||
throw statusCodeError.getException();
|
||||
case StorageServiceService.StorageRecordResult.Success success -> success.getRecords();
|
||||
case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords();
|
||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||
};
|
||||
}
|
||||
@ -625,7 +528,6 @@ public class StorageHelper {
|
||||
storageIds.addAll(account.getUnknownStorageIdStore().getUnknownStorageIds(connection));
|
||||
storageIds.addAll(account.getGroupStore().getStorageIds(connection));
|
||||
storageIds.addAll(account.getRecipientStore().getStorageIds(connection));
|
||||
storageIds.addAll(account.getStickerStore().getStorageIds(connection));
|
||||
storageIds.add(account.getRecipientStore().getSelfStorageId(connection));
|
||||
return storageIds;
|
||||
}
|
||||
@ -674,14 +576,6 @@ public class StorageHelper {
|
||||
account.getUsernameLink());
|
||||
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 -> {
|
||||
throw new AssertionError("Got unknown local storage record type: " + storageId);
|
||||
}
|
||||
@ -736,29 +630,16 @@ public class StorageHelper {
|
||||
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
|
||||
}
|
||||
|
||||
private Pair<List<StorageId>, List<StorageId>> processKnownRecords(
|
||||
private List<StorageId> processKnownRecords(
|
||||
final Connection connection,
|
||||
List<SignalStorageRecord> records,
|
||||
final Set<StorageId> identityConflictsPendingRepair
|
||||
List<SignalStorageRecord> records
|
||||
) throws SQLException {
|
||||
final var unknownRecords = new ArrayList<StorageId>();
|
||||
final var processedRecords = new ArrayList<StorageId>();
|
||||
|
||||
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 groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
|
||||
final var contactRecordProcessor = new ContactRecordProcessor(account,
|
||||
connection,
|
||||
context.getJobExecutor(),
|
||||
identityConflictsPendingRepair);
|
||||
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) {
|
||||
if (record.getProto().account != null) {
|
||||
@ -777,21 +658,12 @@ public class StorageHelper {
|
||||
logger.debug("Reading record {} of type contact", record.getId());
|
||||
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
||||
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 {
|
||||
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.ViewedMessage;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
@ -132,14 +131,11 @@ public class SyncHelper {
|
||||
|
||||
if (groupsFile.exists() && groupsFile.length() > 0) {
|
||||
try (var groupsFileStream = new FileInputStream(groupsFile)) {
|
||||
final var streamDetails = new StreamDetails(groupsFileStream,
|
||||
MimeUtils.OCTET_STREAM,
|
||||
groupsFile.length());
|
||||
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
||||
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
|
||||
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(streamDetails.getStream())
|
||||
.withContentType(streamDetails.getContentType())
|
||||
.withLength(streamDetails.getLength())
|
||||
.withStream(groupsFileStream)
|
||||
.withContentType(MimeUtils.OCTET_STREAM)
|
||||
.withLength(groupsFile.length())
|
||||
.withResumableUploadSpec(uploadSpec)
|
||||
.build();
|
||||
|
||||
@ -160,7 +156,7 @@ public class SyncHelper {
|
||||
|
||||
try {
|
||||
try (OutputStream fos = new FileOutputStream(contactsFile)) {
|
||||
var out = new DeviceContactsOutputStream(fos);
|
||||
var out = new DeviceContactsOutputStream(fos, true, true);
|
||||
for (var contactPair : account.getContactStore().getContacts()) {
|
||||
final var recipientId = contactPair.first();
|
||||
final var contact = contactPair.second();
|
||||
@ -194,14 +190,11 @@ public class SyncHelper {
|
||||
|
||||
if (contactsFile.exists() && contactsFile.length() > 0) {
|
||||
try (var contactsFileStream = new FileInputStream(contactsFile)) {
|
||||
final var streamDetails = new StreamDetails(contactsFileStream,
|
||||
MimeUtils.OCTET_STREAM,
|
||||
contactsFile.length());
|
||||
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
||||
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
|
||||
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(streamDetails.getStream())
|
||||
.withContentType(streamDetails.getContentType())
|
||||
.withLength(streamDetails.getLength())
|
||||
.withStream(contactsFileStream)
|
||||
.withContentType(MimeUtils.OCTET_STREAM)
|
||||
.withLength(contactsFile.length())
|
||||
.withResumableUploadSpec(uploadSpec)
|
||||
.build();
|
||||
|
||||
@ -237,19 +230,18 @@ public class SyncHelper {
|
||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
||||
if (address.aci().isPresent() || address.number().isPresent()) {
|
||||
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
||||
address.number().orElse(null),
|
||||
record.second().blockedAt()));
|
||||
address.number().orElse(null)));
|
||||
}
|
||||
}
|
||||
}
|
||||
var groups = new ArrayList<BlockedListMessage.Group>();
|
||||
var groupIds = new ArrayList<byte[]>();
|
||||
for (var record : account.getGroupStore().getGroups()) {
|
||||
if (record.isBlocked()) {
|
||||
groups.add(new BlockedListMessage.Group(record.getGroupId().serialize(), record.getBlockedAt()));
|
||||
groupIds.add(record.getGroupId().serialize());
|
||||
}
|
||||
}
|
||||
return context.getSendHelper()
|
||||
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groups)));
|
||||
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
|
||||
}
|
||||
|
||||
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.SignalAccount;
|
||||
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.recipients.RecipientAddress;
|
||||
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.ACI;
|
||||
import org.signal.core.models.ServiceId.PNI;
|
||||
import org.signal.core.util.Base64;
|
||||
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.NoSessionException;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
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.SignalServiceReceiptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||
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.OfferMessage;
|
||||
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.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.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.util.Utils.handleResponseException;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
|
||||
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
|
||||
|
||||
public class ManagerImpl implements Manager {
|
||||
@ -196,7 +190,6 @@ public class ManagerImpl implements Manager {
|
||||
userAgent,
|
||||
account.getCredentialsProvider(),
|
||||
account.getSignalServiceDataStore(),
|
||||
account.getDeviceId(),
|
||||
executor,
|
||||
sessionLock);
|
||||
final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
|
||||
@ -250,11 +243,6 @@ public class ManagerImpl implements Manager {
|
||||
return account.getNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSelfACI() {
|
||||
return account.getAci().toString();
|
||||
}
|
||||
|
||||
public void checkAccountState() throws IOException {
|
||||
context.getAccountHelper().checkAccountState();
|
||||
final var lastRecipientsRefresh = account.getLastRecipientsRefresh();
|
||||
@ -290,7 +278,7 @@ public class ManagerImpl implements Manager {
|
||||
registeredUsers = context.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet);
|
||||
} catch (CdsiResourceExhaustedException e) {
|
||||
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 -> {
|
||||
@ -484,26 +472,23 @@ public class ManagerImpl implements Manager {
|
||||
|
||||
@Override
|
||||
public List<Device> getLinkedDevices() throws IOException {
|
||||
final List<LinkedDevice> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||
.getDevices(cont));
|
||||
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||
account.setMultiDevice(devices.size() > 1);
|
||||
var identityKey = account.getAciIdentityKeyPair();
|
||||
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||
return devices.stream().map(d -> {
|
||||
String deviceName = null;
|
||||
if (d.getEncryptedName() != null && d.getEncryptedName().length > 0) {
|
||||
String deviceName = d.getName();
|
||||
if (deviceName != null) {
|
||||
try {
|
||||
deviceName = new String(DeviceNameCipher.decryptDeviceName(DeviceName.ADAPTER.decode(d.getEncryptedName()),
|
||||
identityKey), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
|
||||
} catch (IOException 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);
|
||||
return new Device(d.getId(),
|
||||
deviceName,
|
||||
createdAt == null ? 0 : createdAt,
|
||||
d.getLastSeen().toEpochMilli(),
|
||||
d.getLastSeen(),
|
||||
d.getId() == account.getDeviceId());
|
||||
}).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";
|
||||
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||
try {
|
||||
@ -531,9 +516,8 @@ public class ManagerImpl implements Manager {
|
||||
var associatedData = new ByteArrayOutputStream();
|
||||
associatedData.write(d.getId());
|
||||
associatedData.write(ByteBuffer.allocate(4).putInt(d.getRegistrationId()).array());
|
||||
var createdAtPlaintext = identityKey.open(d.getCreatedAtCiphertext(),
|
||||
DECRYPTION_INFO,
|
||||
associatedData.toByteArray());
|
||||
var createdAtPlaintext = identityKey.open(Base64.decode(d.getCreatedAtCiphertext()
|
||||
.getBytes(StandardCharsets.UTF_8)), DECRYPTION_INFO, associatedData.toByteArray());
|
||||
return ByteBuffer.wrap(createdAtPlaintext).getLong();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed while reading the protobuf.", e);
|
||||
@ -610,11 +594,6 @@ public class ManagerImpl implements Manager {
|
||||
context.getGroupHelper().deleteGroup(groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendGroupMessageResults terminateGroup(GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
||||
return context.getGroupHelper().terminateGroup(groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String name,
|
||||
@ -803,20 +782,13 @@ public class ManagerImpl implements Manager {
|
||||
) {
|
||||
try {
|
||||
final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
|
||||
List<SendMessageResult> results;
|
||||
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 result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
|
||||
|
||||
final var aci = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).aci();
|
||||
if (aci.isPresent()) {
|
||||
context.getSyncHelper().sendSyncReceiptMessage(aci.get(), receiptMessage);
|
||||
}
|
||||
return new SendMessageResults(timestamp, Map.of(sender, results));
|
||||
return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result))));
|
||||
} catch (UnregisteredRecipientException e) {
|
||||
return new SendMessageResults(timestamp,
|
||||
Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress()))));
|
||||
@ -850,98 +822,6 @@ public class ManagerImpl implements Manager {
|
||||
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(
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final Message message
|
||||
@ -953,10 +833,10 @@ public class ManagerImpl implements Manager {
|
||||
final var remainder = result.getSecond();
|
||||
if (remainder != null) {
|
||||
final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
|
||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||
final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
|
||||
MimeUtils.LONG_TEXT,
|
||||
messageBytes.length);
|
||||
final var uploadSpec = context.getAttachmentHelper().getResumableUploadSpec(streamDetails);
|
||||
final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
||||
Optional.empty(),
|
||||
uploadSpec);
|
||||
@ -970,10 +850,7 @@ public class ManagerImpl implements Manager {
|
||||
}
|
||||
if (!message.attachments().isEmpty()) {
|
||||
final var uploadedAttachments = context.getAttachmentHelper()
|
||||
.uploadAttachments(message.attachments(),
|
||||
message.attachmentDimensions(),
|
||||
message.attachmentBlurHashes(),
|
||||
message.voiceNote());
|
||||
.uploadAttachments(message.attachments(), message.voiceNote());
|
||||
if (!additionalAttachments.isEmpty()) {
|
||||
additionalAttachments.addAll(uploadedAttachments);
|
||||
messageBuilder.withAttachments(additionalAttachments);
|
||||
@ -1027,7 +904,7 @@ public class ManagerImpl implements Manager {
|
||||
if (streamDetails == null) {
|
||||
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,
|
||||
Optional.empty(),
|
||||
uploadSpec);
|
||||
@ -1211,26 +1088,30 @@ public class ManagerImpl implements Manager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
|
||||
for (var recipient : recipients) {
|
||||
final RecipientId recipientId;
|
||||
try {
|
||||
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||
} catch (UnregisteredRecipientException e) {
|
||||
continue;
|
||||
}
|
||||
final var recipientAddress = context.getAccount()
|
||||
.getRecipientAddressResolver()
|
||||
.resolveRecipientAddress(recipientId);
|
||||
final var aciSessionStore = account.getAccountData(ServiceIdType.ACI).getSessionStore();
|
||||
final var pniSessionStore = account.getAccountData(ServiceIdType.PNI).getSessionStore();
|
||||
if (recipientAddress.aci().isPresent()) {
|
||||
aciSessionStore.archiveSessions(recipientAddress.aci().get());
|
||||
pniSessionStore.archiveSessions(recipientAddress.aci().get());
|
||||
}
|
||||
if (recipientAddress.pni().isPresent()) {
|
||||
aciSessionStore.archiveSessions(recipientAddress.pni().get());
|
||||
pniSessionStore.archiveSessions(recipientAddress.pni().get());
|
||||
public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
|
||||
var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
|
||||
|
||||
try {
|
||||
return sendMessage(messageBuilder,
|
||||
recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()),
|
||||
false);
|
||||
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
|
||||
throw new AssertionError(e);
|
||||
} finally {
|
||||
for (var recipient : recipients) {
|
||||
final RecipientId recipientId;
|
||||
try {
|
||||
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||
} catch (UnregisteredRecipientException e) {
|
||||
continue;
|
||||
}
|
||||
final var serviceId = context.getAccount()
|
||||
.getRecipientAddressResolver()
|
||||
.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);
|
||||
if (contact != null) {
|
||||
final var nickname = contact.getDisplayNickname();
|
||||
if (!Util.isEmpty(nickname)) {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(contact.getName())) {
|
||||
return contact.getName();
|
||||
}
|
||||
if (contact != null && !Util.isEmpty(contact.getName())) {
|
||||
return contact.getName();
|
||||
}
|
||||
|
||||
final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
|
||||
@ -1944,10 +1818,8 @@ public class ManagerImpl implements Manager {
|
||||
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, null);
|
||||
try {
|
||||
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);
|
||||
} 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);
|
||||
try {
|
||||
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);
|
||||
} 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);
|
||||
try {
|
||||
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);
|
||||
} 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);
|
||||
try {
|
||||
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);
|
||||
} 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);
|
||||
try {
|
||||
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);
|
||||
} 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.api.AccountCheckException;
|
||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
||||
import org.signal.core.util.UuidUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -39,6 +38,13 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
managers.forEach(m -> m.addClosedListener(() -> this.removeManager(m)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAccountNumbers() {
|
||||
synchronized (managers) {
|
||||
return managers.stream().map(Manager::getSelfNumber).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Manager> getManagers() {
|
||||
synchronized (managers) {
|
||||
@ -89,47 +95,23 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manager getManager(final String identifier) {
|
||||
public Manager getManager(final String number) {
|
||||
synchronized (managers) {
|
||||
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
||||
// Check if UUID corresponds to an already-loaded manager
|
||||
final var existing = managers.stream()
|
||||
.filter(m -> m.getSelfACI().equals(identifier))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (existing != null) {
|
||||
logger.debug("Found already loaded manager for ACI: {}", identifier);
|
||||
return existing;
|
||||
}
|
||||
// Load by ACI
|
||||
try {
|
||||
final var newManager = signalAccountFiles.initManagerByAci(identifier);
|
||||
managers.add(newManager);
|
||||
return newManager;
|
||||
} catch (NotRegisteredException e) {
|
||||
logger.debug("Manager not found by ACI: {}", identifier);
|
||||
} catch (IOException | IllegalArgumentException | AccountCheckException e) {
|
||||
logger.warn("Failed to load new manager by ACI: {}", identifier, e);
|
||||
}
|
||||
} else {
|
||||
// Phone number — check already loaded managers
|
||||
var existing = managers.stream()
|
||||
.filter(m -> identifier.equals(m.getSelfNumber()))
|
||||
.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);
|
||||
}
|
||||
final var manager = managers.stream()
|
||||
.filter(m -> m.getSelfNumber().equals(number))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (manager != null) {
|
||||
return manager;
|
||||
}
|
||||
try {
|
||||
final var newManager = signalAccountFiles.initManager(number);
|
||||
managers.add(newManager);
|
||||
return newManager;
|
||||
} catch (IOException | NotRegisteredException | AccountCheckException e) {
|
||||
logger.warn("Failed to load new manager", e);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,59 +19,34 @@ package org.asamk.signal.manager.internal;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.ProvisioningManager;
|
||||
import org.asamk.signal.manager.Settings;
|
||||
import org.asamk.signal.manager.api.BadRequestException;
|
||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||
import org.asamk.signal.manager.api.UserAlreadyExistsException;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.signal.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.ecc.ECPrivateKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.network.api.RegistrationApiV2;
|
||||
import org.signal.network.api.RegistrationApiV2.LinkDeviceResponse;
|
||||
import org.signal.network.api.RegistrationApiV2.RegisterAsLinkedDeviceError;
|
||||
import org.signal.network.rest.SignalRestClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningSocket;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher;
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage;
|
||||
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
|
||||
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.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
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.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.Utils.handleResponseExceptionSuspend;
|
||||
|
||||
public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
public class ProvisioningManagerImpl implements ProvisioningManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
|
||||
|
||||
@ -81,10 +56,9 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
private final Consumer<Manager> newManagerListener;
|
||||
private final AccountsStore accountsStore;
|
||||
|
||||
private final ProvisioningApi provisioningApi;
|
||||
private final IdentityKeyPair tempIdentityKey;
|
||||
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(
|
||||
PathConfig pathConfig,
|
||||
@ -99,72 +73,46 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
this.newManagerListener = newManagerListener;
|
||||
this.accountsStore = accountsStore;
|
||||
|
||||
final IdentityKeyPair tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||
password = KeyUtils.createPassword();
|
||||
|
||||
socketHandle = ProvisioningSocket.Companion.start(new ProvisioningSocket.Mode.Link(false),
|
||||
tempIdentityKey,
|
||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
(id, t) -> {
|
||||
urlFuture.completeExceptionally(t);
|
||||
messageFuture.completeExceptionally(t);
|
||||
},
|
||||
new ProvisioningBlock());
|
||||
final var credentialsProvider = new DynamicCredentialsProvider(null,
|
||||
null,
|
||||
null,
|
||||
password,
|
||||
SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
credentialsProvider,
|
||||
userAgent,
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
userAgent);
|
||||
this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
||||
try {
|
||||
var url = urlFuture.get(30, TimeUnit.SECONDS);
|
||||
// Mode.Link(false) does not advertise any capabilities itself.
|
||||
return new URI(url + "&capabilities=nopni");
|
||||
} 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);
|
||||
}
|
||||
var deviceUuid = provisioningApi.getNewDeviceUuid();
|
||||
|
||||
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
||||
SecondaryProvisioningCipher.ProvisioningDecryptResult<ProvisionMessage> decryptResult;
|
||||
try {
|
||||
decryptResult = messageFuture.get(120, TimeUnit.SECONDS);
|
||||
} catch (java.util.concurrent.TimeoutException e) {
|
||||
throw new TimeoutException("Timed out waiting for provisioning message");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while waiting for provisioning message", e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new IOException("Failed to receive provisioning message", e.getCause());
|
||||
}
|
||||
var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
|
||||
var number = ret.getNumber();
|
||||
var aci = ret.getAci();
|
||||
var pni = ret.getPni();
|
||||
|
||||
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 = parsePni(msg);
|
||||
var identifier = number != null ? number : aci.toString();
|
||||
|
||||
logger.info("Received link information from {}, linking in progress ...", identifier);
|
||||
logger.info("Received link information from {}, linking in progress ...", number);
|
||||
|
||||
var accountPath = accountsStore.getPathByAci(aci);
|
||||
if (accountPath == null && number != null) {
|
||||
if (accountPath == null) {
|
||||
accountPath = accountsStore.getPathByNumber(number);
|
||||
}
|
||||
final var accountExists = accountPath != null && SignalAccount.accountFileExists(pathConfig.dataPath(),
|
||||
accountPath);
|
||||
if (accountExists && !canRelinkExistingAccount(accountPath)) {
|
||||
throw new UserAlreadyExistsException(identifier,
|
||||
SignalAccount.getFileName(pathConfig.dataPath(), accountPath));
|
||||
throw new UserAlreadyExistsException(number, SignalAccount.getFileName(pathConfig.dataPath(), accountPath));
|
||||
}
|
||||
if (accountPath == null) {
|
||||
accountPath = accountsStore.addAccount(number, aci);
|
||||
@ -172,44 +120,21 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
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 = pni == null
|
||||
? null
|
||||
: 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
|
||||
? null
|
||||
: DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), aciIdentity);
|
||||
var accountEntropyPool = msg.accountEntropyPool == null ? null : new AccountEntropyPool(msg.accountEntropyPool);
|
||||
var mediaRootBackupKey = msg.mediaRootBackupKey == null
|
||||
? null
|
||||
: new MediaRootBackupKey(msg.mediaRootBackupKey.toByteArray());
|
||||
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getAciIdentity().getPrivateKey());
|
||||
// Create new account with the synced identity
|
||||
var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
|
||||
|
||||
SignalAccount account = null;
|
||||
var cleanUpPartialAccountOnFailure = false;
|
||||
var linkingFinished = false;
|
||||
try {
|
||||
if (!accountExists) {
|
||||
account = SignalAccount.createLinkedAccount(pathConfig.dataPath(),
|
||||
accountPath,
|
||||
serviceEnvironmentConfig.type(),
|
||||
Settings.DEFAULT);
|
||||
cleanUpPartialAccountOnFailure = true;
|
||||
} else {
|
||||
account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, Settings.DEFAULT);
|
||||
cleanUpPartialAccountOnFailure = false;
|
||||
}
|
||||
|
||||
account.setProvisioningData(number,
|
||||
@ -217,34 +142,24 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
pni,
|
||||
password,
|
||||
encryptedDeviceName,
|
||||
aciIdentity,
|
||||
pniIdentity,
|
||||
ret.getAciIdentity(),
|
||||
ret.getPniIdentity(),
|
||||
profileKey,
|
||||
accountEntropyPool,
|
||||
msg.authCredentialSalt == null ? null : msg.authCredentialSalt.toByteArray(),
|
||||
mediaRootBackupKey);
|
||||
ret.getAccountEntropyPool(),
|
||||
ret.getMediaRootBackupKey());
|
||||
|
||||
if (msg.readReceipts != null) {
|
||||
account.getConfigurationStore().setReadReceipts(msg.readReceipts);
|
||||
}
|
||||
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
|
||||
|
||||
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||
final var pniPreKeys = pni == null
|
||||
? null
|
||||
: generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||
|
||||
logger.debug("Finishing new device registration");
|
||||
final var restClient = new SignalRestClient(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
userAgent);
|
||||
final var registrationApi = new RegistrationApiV2(restClient, false);
|
||||
final var deviceId = registerLinkedDevice(registrationApi,
|
||||
account,
|
||||
msg.provisioningCode,
|
||||
var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
||||
account.getAccountAttributes(null),
|
||||
aciPreKeys,
|
||||
pniPreKeys);
|
||||
|
||||
account.finishLinking(deviceId, aciPreKeys, pniPreKeys);
|
||||
linkingFinished = true;
|
||||
|
||||
ManagerImpl m = null;
|
||||
try {
|
||||
@ -275,18 +190,12 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
newManagerListener.accept(m);
|
||||
m = null;
|
||||
}
|
||||
return identifier;
|
||||
return number;
|
||||
} finally {
|
||||
if (m != null) {
|
||||
m.close();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (!linkingFinished && cleanUpPartialAccountOnFailure && account != null) {
|
||||
cleanupPartialAccount(account, accountPath, e);
|
||||
account = null;
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
if (account != null) {
|
||||
account.close();
|
||||
@ -294,98 +203,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
static PNI parsePni(final ProvisionMessage message) throws IOException {
|
||||
if (message.number != null) {
|
||||
return PNI.parseOrThrow(message.pni, message.pniBinary);
|
||||
}
|
||||
if (message.pni != null
|
||||
|| message.pniBinary != null
|
||||
|| message.pniIdentityKeyPublic != null
|
||||
|| message.pniIdentityKeyPrivate != null) {
|
||||
throw new IOException("Provisioning message has PNI material without a phone number");
|
||||
}
|
||||
if (message.authCredentialSalt == null || message.authCredentialSalt.size() == 0) {
|
||||
throw new IOException("Numberless provisioning message is missing the group auth credential salt");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int registerLinkedDevice(
|
||||
final RegistrationApiV2 registrationApi,
|
||||
final SignalAccount account,
|
||||
final String provisioningCode,
|
||||
final PreKeyCollection aciPreKeys,
|
||||
final PreKeyCollection pniPreKeys
|
||||
) throws IOException {
|
||||
final var attrs = account.getAccountAttributesV2();
|
||||
final var deviceAttributes = new RegistrationApiV2.DeviceAttributes(attrs.getFetchesMessages(),
|
||||
attrs.getRegistrationId(),
|
||||
attrs.getPniRegistrationId(),
|
||||
attrs.getName(),
|
||||
attrs.getCapabilities());
|
||||
try {
|
||||
final LinkDeviceResponse result = handleResponseExceptionSuspend(cont -> registrationApi.registerAsSecondaryDevice(
|
||||
account.getAci(),
|
||||
account.getPassword(),
|
||||
provisioningCode,
|
||||
deviceAttributes,
|
||||
toRegistrationPreKeys(aciPreKeys),
|
||||
toRegistrationPreKeys(pniPreKeys),
|
||||
null,
|
||||
cont));
|
||||
return result.getDeviceId();
|
||||
} catch (BadRequestException e) {
|
||||
throw switch (e.getError()) {
|
||||
case RegisterAsLinkedDeviceError.IncorrectVerification ignored ->
|
||||
new AuthorizationFailedException(403, "Device verification failed");
|
||||
case RegisterAsLinkedDeviceError.MissingCapability ignored ->
|
||||
new IOException("Linked device is missing a required account capability");
|
||||
case RegisterAsLinkedDeviceError.MaxLinkedDevices ignored ->
|
||||
new IOException("Account has reached its linked device limit");
|
||||
case RegisterAsLinkedDeviceError.InvalidRequest ignored ->
|
||||
new IOException("Signal rejected the device linking request");
|
||||
case RegisterAsLinkedDeviceError.RateLimited ignored ->
|
||||
new IOException("Device linking rate limited; try again later");
|
||||
default -> new IOException("Unexpected device linking response");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static RegistrationApiV2.PreKeyCollection toRegistrationPreKeys(final PreKeyCollection preKeys) {
|
||||
return preKeys == null
|
||||
? null
|
||||
: new RegistrationApiV2.PreKeyCollection(preKeys.getIdentityKey(),
|
||||
preKeys.getSignedPreKey(),
|
||||
preKeys.getLastResortKyberPreKey());
|
||||
}
|
||||
|
||||
@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 {
|
||||
final SignalAccount signalAccount;
|
||||
try {
|
||||
@ -399,10 +216,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
}
|
||||
|
||||
try (signalAccount) {
|
||||
if (signalAccount.getDeviceId() <= 0) {
|
||||
logger.debug("Account has invalid deviceId {}, allowing relink.", signalAccount.getDeviceId());
|
||||
return true;
|
||||
}
|
||||
if (signalAccount.isPrimaryDevice()) {
|
||||
logger.debug("Account is a primary device.");
|
||||
return false;
|
||||
@ -430,36 +243,4 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,14 +18,12 @@ package org.asamk.signal.manager.internal;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
import org.asamk.signal.manager.api.BadRequestException;
|
||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.api.TotpRequiredException;
|
||||
import org.asamk.signal.manager.api.UpdateProfile;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
@ -35,14 +33,10 @@ import org.asamk.signal.manager.helper.PinHelper;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||
import org.signal.core.models.AccountEntropyPool;
|
||||
import org.signal.core.models.MasterKey;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.signal.core.models.ServiceId.PNI;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.network.api.RegistrationApiV2;
|
||||
import org.signal.network.api.RegistrationApiV2.RegisterAccountError;
|
||||
import org.signal.network.rest.SignalRestClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
@ -56,25 +50,15 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.asamk.signal.manager.internal.ProvisioningManagerImpl.toRegistrationPreKeys;
|
||||
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
|
||||
|
||||
public class RegistrationManagerImpl implements RegistrationManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class);
|
||||
|
||||
private static final class RecoveryRequestFailedException extends IOException {
|
||||
|
||||
private RecoveryRequestFailedException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
private SignalAccount account;
|
||||
private final PathConfig pathConfig;
|
||||
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
|
||||
@ -123,7 +107,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||
boolean voiceVerification,
|
||||
String captcha,
|
||||
final boolean forceRegister
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, TotpRequiredException, VerificationMethodNotAvailableException {
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
|
||||
if (account.isRegistered()
|
||||
&& account.getServiceEnvironment() != null
|
||||
&& account.getServiceEnvironment() != serviceEnvironmentConfig.type()) {
|
||||
@ -145,13 +129,6 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||
if (recoveryPassword != null && account.isPrimaryDevice() && attemptReregisterAccount(recoveryPassword)) {
|
||||
return;
|
||||
}
|
||||
if (account.getAci() != null && account.getAccountEntropyPool() != null && attemptRecoverAccount(null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (account.getNumber() == null) {
|
||||
throw new IOException("Failed to recover account using its ACI and Account Entropy Pool");
|
||||
}
|
||||
|
||||
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
||||
logger.trace("Creating verification session");
|
||||
@ -207,108 +184,6 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||
finishAccountRegistration(response, pin, masterKey, aciPreKeys, pniPreKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWithRecoveryKey(
|
||||
final String recoveryKey,
|
||||
final boolean forceRegister,
|
||||
final Integer totp
|
||||
) throws IOException, RateLimitException, TotpRequiredException {
|
||||
if (account.getAci() == null) {
|
||||
throw new IOException("Recovery-key registration requires an ACI account identifier");
|
||||
}
|
||||
if (account.isRegistered() && !forceRegister) {
|
||||
throw new IOException("Account is already registered; use --reregister to register it again");
|
||||
}
|
||||
final var accountEntropyPool = AccountEntropyPool.Companion.parseOrNull(recoveryKey);
|
||||
if (accountEntropyPool == null || !AccountEntropyPool.Companion.isFullyValid(accountEntropyPool.getValue())) {
|
||||
throw new IOException("Invalid recovery key");
|
||||
}
|
||||
recoverAccount(totp, false, accountEntropyPool);
|
||||
}
|
||||
|
||||
private boolean attemptRecoverAccount(
|
||||
final Integer totp
|
||||
) throws IOException, RateLimitException, TotpRequiredException {
|
||||
final var accountEntropyPool = account.getAccountEntropyPool();
|
||||
try {
|
||||
recoverAccount(totp, false, accountEntropyPool);
|
||||
logger.info("Reregistered existing account using its ACI and Account Entropy Pool.");
|
||||
return true;
|
||||
} catch (TotpRequiredException | RateLimitException e) {
|
||||
throw e;
|
||||
} catch (RecoveryRequestFailedException e) {
|
||||
logger.debug("Failed to reregister account using its ACI and Account Entropy Pool", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void recoverAccount(
|
||||
final Integer totp,
|
||||
final boolean includeRegistrationLock,
|
||||
final AccountEntropyPool accountEntropyPool
|
||||
) throws IOException, RateLimitException, TotpRequiredException {
|
||||
if (account.getPniIdentityKeyPair() == null) {
|
||||
account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
|
||||
}
|
||||
|
||||
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||
final var masterKey = accountEntropyPool.deriveMasterKey();
|
||||
final var recoveryPassword = masterKey.deriveRegistrationRecoveryPassword();
|
||||
final var registrationLock = includeRegistrationLock ? masterKey.deriveRegistrationLock() : null;
|
||||
final var restClient = new SignalRestClient(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent);
|
||||
final var registrationApi = new RegistrationApiV2(restClient, true);
|
||||
|
||||
final RegistrationApiV2.RegisterAccountResponse response;
|
||||
try {
|
||||
response = handleResponseExceptionSuspend(cont -> registrationApi.registerAccount(null,
|
||||
account.getPassword(),
|
||||
null,
|
||||
recoveryPassword,
|
||||
null,
|
||||
account.getAccountAttributesV2ForRecovery(registrationLock, recoveryPassword),
|
||||
toRegistrationPreKeys(aciPreKeys),
|
||||
toRegistrationPreKeys(pniPreKeys),
|
||||
null,
|
||||
true,
|
||||
account.getAci(),
|
||||
totp,
|
||||
cont));
|
||||
} catch (BadRequestException e) {
|
||||
switch (e.getError()) {
|
||||
case RegisterAccountError.RegistrationLock ignored -> {
|
||||
if (includeRegistrationLock) {
|
||||
throw new RecoveryRequestFailedException("Registration lock recovery failed", e);
|
||||
}
|
||||
recoverAccount(totp, true, accountEntropyPool);
|
||||
return;
|
||||
}
|
||||
case RegisterAccountError.TotpMissingOrIncorrect ignored -> throw new TotpRequiredException();
|
||||
case RegisterAccountError.RegistrationRecoveryPasswordIncorrect ignored ->
|
||||
throw new RecoveryRequestFailedException("Account key or recovery key is incorrect", e);
|
||||
case RegisterAccountError.RateLimited ignored -> throw new RateLimitException(null);
|
||||
case RegisterAccountError.PostQuantumRatchetRequired ignored ->
|
||||
throw new IOException("signal-cli is too old to register this account", e);
|
||||
default -> throw new IOException("Signal rejected recovery-key registration", e);
|
||||
}
|
||||
}
|
||||
|
||||
final var aci = ACI.parseOrThrow(response.getAci());
|
||||
final var pni = response.getPni() == null ? null : PNI.parseOrThrow(response.getPni());
|
||||
final var authCredentialSalt = response.getAuthCredentialSalt() == null
|
||||
? null
|
||||
: Base64.getDecoder().decode(response.getAuthCredentialSalt());
|
||||
account.finishRecoveryRegistration(aci,
|
||||
pni,
|
||||
response.getE164(),
|
||||
accountEntropyPool,
|
||||
authCredentialSalt,
|
||||
aciPreKeys,
|
||||
pni == null ? null : pniPreKeys);
|
||||
accountFileUpdater.updateAccountIdentifiers(response.getE164(), aci);
|
||||
finishManagerRegistration(response.getStorageCapable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteLocalAccountData() throws IOException {
|
||||
account.deleteAccountData();
|
||||
@ -356,7 +231,6 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||
userAgent,
|
||||
account.getCredentialsProvider(),
|
||||
account.getSignalServiceDataStore(),
|
||||
0,
|
||||
null,
|
||||
new ReentrantSignalSessionLock());
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
@ -416,17 +290,13 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||
account.finishRegistration(aci, pni, masterKey, pin, aciPreKeys, pniPreKeys);
|
||||
accountFileUpdater.updateAccountIdentifiers(account.getNumber(), aci);
|
||||
|
||||
finishManagerRegistration(response.isStorageCapable());
|
||||
}
|
||||
|
||||
private void finishManagerRegistration(final boolean storageCapable) throws IOException {
|
||||
ManagerImpl m = null;
|
||||
try {
|
||||
m = new ManagerImpl(account, pathConfig, accountFileUpdater, serviceEnvironmentConfig, userAgent);
|
||||
account = null;
|
||||
|
||||
m.refreshPreKeys();
|
||||
if (storageCapable) {
|
||||
if (response.isStorageCapable()) {
|
||||
m.syncRemoteStorage();
|
||||
}
|
||||
// Set an initial empty profile so user can be added to groups
|
||||
|
||||
@ -3,22 +3,9 @@ package org.asamk.signal.manager.internal;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.core.util.UptimeSleepTimer;
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.network.api.AccountApiV2;
|
||||
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.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
@ -27,21 +14,29 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
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.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
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.profiles.ProfileApi;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
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.services.ProfileService;
|
||||
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.username.UsernameApi;
|
||||
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.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
||||
@ -66,7 +61,6 @@ public class SignalDependencies {
|
||||
private final String userAgent;
|
||||
private final CredentialsProvider credentialsProvider;
|
||||
private final SignalServiceDataStore dataStore;
|
||||
private final int deviceId;
|
||||
private final ExecutorService executor;
|
||||
private final SignalSessionLock sessionLock;
|
||||
|
||||
@ -74,7 +68,6 @@ public class SignalDependencies {
|
||||
|
||||
private SignalServiceAccountManager accountManager;
|
||||
private AccountApi accountApi;
|
||||
private AccountApiV2 accountApiV2;
|
||||
private RateLimitChallengeApi rateLimitChallengeApi;
|
||||
private CdsApi cdsApi;
|
||||
private UsernameApi usernameApi;
|
||||
@ -89,11 +82,6 @@ public class SignalDependencies {
|
||||
private KeysApi keysApi;
|
||||
private GroupsV2Operations groupsV2Operations;
|
||||
private ClientZkOperations clientZkOperations;
|
||||
private ProfileService profileService;
|
||||
private ProfileApi profileApi;
|
||||
private CdnService cdnService;
|
||||
private PreKeyRepository preKeyRepository;
|
||||
private SignalRestClient signalRestClient;
|
||||
|
||||
private PushServiceSocket pushServiceSocket;
|
||||
private Network libSignalNetwork;
|
||||
@ -103,13 +91,14 @@ public class SignalDependencies {
|
||||
private SignalServiceMessageSender messageSender;
|
||||
|
||||
private List<SecureValueRecovery> secureValueRecovery;
|
||||
private ProfileService profileService;
|
||||
private ProfileApi profileApi;
|
||||
|
||||
SignalDependencies(
|
||||
final ServiceEnvironmentConfig serviceEnvironmentConfig,
|
||||
final String userAgent,
|
||||
final CredentialsProvider credentialsProvider,
|
||||
final SignalServiceDataStore dataStore,
|
||||
final int deviceId,
|
||||
final ExecutorService executor,
|
||||
final SignalSessionLock sessionLock
|
||||
) {
|
||||
@ -117,7 +106,6 @@ public class SignalDependencies {
|
||||
this.userAgent = userAgent;
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
this.dataStore = dataStore;
|
||||
this.deviceId = deviceId;
|
||||
this.executor = executor;
|
||||
this.sessionLock = sessionLock;
|
||||
}
|
||||
@ -223,11 +211,6 @@ public class SignalDependencies {
|
||||
return getOrCreate(() -> accountApi, () -> accountApi = new AccountApi(getAuthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public AccountApiV2 getAccountApiV2() {
|
||||
return getOrCreate(() -> accountApiV2,
|
||||
() -> accountApiV2 = new AccountApiV2(getAuthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public RateLimitChallengeApi getRateLimitChallengeApi() {
|
||||
return getOrCreate(() -> rateLimitChallengeApi,
|
||||
() -> rateLimitChallengeApi = new RateLimitChallengeApi(getAuthenticatedSignalWebSocket()));
|
||||
@ -260,8 +243,8 @@ public class SignalDependencies {
|
||||
getPushServiceSocket()));
|
||||
}
|
||||
|
||||
public StorageServiceService getStorageServiceRepository() {
|
||||
return new StorageServiceService(getStorageServiceApi());
|
||||
public StorageServiceRepository getStorageServiceRepository() {
|
||||
return new StorageServiceRepository(getStorageServiceApi());
|
||||
}
|
||||
|
||||
public CertificateApi getCertificateApi() {
|
||||
@ -318,7 +301,7 @@ public class SignalDependencies {
|
||||
getLibSignalNetwork(),
|
||||
credentialsProvider,
|
||||
allowStories,
|
||||
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(30));
|
||||
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
||||
healthMonitor.monitor(authenticatedSignalWebSocket);
|
||||
});
|
||||
}
|
||||
@ -333,7 +316,7 @@ public class SignalDependencies {
|
||||
getLibSignalNetwork(),
|
||||
null,
|
||||
allowStories,
|
||||
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(30));
|
||||
healthMonitor), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
||||
healthMonitor.monitor(unauthenticatedSignalWebSocket);
|
||||
});
|
||||
}
|
||||
@ -343,34 +326,12 @@ public class SignalDependencies {
|
||||
() -> 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() {
|
||||
return getOrCreate(() -> messageSender,
|
||||
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
|
||||
dataStore,
|
||||
sessionLock,
|
||||
getAttachmentApi(),
|
||||
getMessageApi(),
|
||||
getKeysApi(),
|
||||
Optional.empty(),
|
||||
@ -378,7 +339,8 @@ public class SignalDependencies {
|
||||
ServiceConfig.MAX_ENVELOPE_SIZE,
|
||||
ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE,
|
||||
() -> true,
|
||||
getPreKeyRepository()));
|
||||
true,
|
||||
true));
|
||||
}
|
||||
|
||||
public List<SecureValueRecovery> getSecureValueRecovery() {
|
||||
@ -406,10 +368,7 @@ public class SignalDependencies {
|
||||
|
||||
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
|
||||
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots());
|
||||
final var serviceId = serviceIdType == ServiceIdType.ACI
|
||||
? credentialsProvider.getAci()
|
||||
: credentialsProvider.getPni();
|
||||
final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164());
|
||||
final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164());
|
||||
final var deviceId = credentialsProvider.getDeviceId();
|
||||
return new SignalServiceCipher(address,
|
||||
deviceId,
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
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.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.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -96,26 +94,6 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
||||
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
|
||||
* the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
|
||||
|
||||
@ -9,15 +9,9 @@ public class DownloadProfileJob implements Job {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DownloadProfileJob.class);
|
||||
private final RecipientAddress address;
|
||||
private final boolean resolveIdentityKeyConflict;
|
||||
|
||||
public DownloadProfileJob(RecipientAddress address) {
|
||||
this(address, false);
|
||||
}
|
||||
|
||||
public DownloadProfileJob(RecipientAddress address, boolean resolveIdentityKeyConflict) {
|
||||
this.address = address;
|
||||
this.resolveIdentityKeyConflict = resolveIdentityKeyConflict;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -25,9 +19,6 @@ public class DownloadProfileJob implements Job {
|
||||
logger.trace("Refreshing profile for {}", address);
|
||||
final var account = context.getAccount();
|
||||
final var recipientId = account.getRecipientStore().resolveRecipient(address);
|
||||
final var refreshed = context.getProfileHelper().refreshRecipientProfile(recipientId);
|
||||
if (refreshed && resolveIdentityKeyConflict) {
|
||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||
}
|
||||
context.getProfileHelper().refreshRecipientProfile(recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ import java.util.UUID;
|
||||
public class AccountDatabase extends Database {
|
||||
|
||||
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) {
|
||||
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(
|
||||
|
||||
@ -44,8 +44,7 @@ public class AttachmentStore {
|
||||
}
|
||||
|
||||
public StreamDetails retrieveAttachment(final String id) throws IOException {
|
||||
final var safeId = sanitizeId(id);
|
||||
final var attachmentFile = new File(attachmentsPath, safeId);
|
||||
final var attachmentFile = new File(attachmentsPath, id);
|
||||
return Utils.createStreamDetailsFromFile(attachmentFile);
|
||||
}
|
||||
|
||||
@ -62,8 +61,7 @@ public class AttachmentStore {
|
||||
Optional<String> contentType
|
||||
) {
|
||||
final var extension = getAttachmentExtension(filename, contentType);
|
||||
final var safe = sanitizeId(attachmentId.toString());
|
||||
return new File(attachmentsPath, safe + extension + ".preview");
|
||||
return new File(attachmentsPath, attachmentId.toString() + extension + ".preview");
|
||||
}
|
||||
|
||||
private File getAttachmentFile(
|
||||
@ -72,15 +70,7 @@ public class AttachmentStore {
|
||||
Optional<String> contentType
|
||||
) {
|
||||
final var extension = getAttachmentExtension(filename, contentType);
|
||||
final var safe = sanitizeId(attachmentId.toString());
|
||||
return new File(attachmentsPath, safe + extension);
|
||||
}
|
||||
|
||||
private static String sanitizeId(final String id) {
|
||||
if (id == null) {
|
||||
return "";
|
||||
}
|
||||
return id.replaceAll("[^A-Za-z0-9_.-]", "_");
|
||||
return new File(attachmentsPath, attachmentId.toString() + extension);
|
||||
}
|
||||
|
||||
private static String getAttachmentExtension(final Optional<String> filename, final Optional<String> contentType) {
|
||||
|
||||
@ -71,7 +71,6 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.protocol.util.KeyHelper;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.network.api.RegistrationApiV2;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
|
||||
@ -117,7 +116,7 @@ public class SignalAccount implements Closeable {
|
||||
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||
|
||||
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();
|
||||
|
||||
@ -142,7 +141,6 @@ public class SignalAccount implements Closeable {
|
||||
private MasterKey pinMasterKey;
|
||||
private StorageKey storageKey;
|
||||
private AccountEntropyPool accountEntropyPool;
|
||||
private byte[] authCredentialSalt;
|
||||
private MediaRootBackupKey mediaRootBackupKey;
|
||||
private ProfileKey profileKey;
|
||||
|
||||
@ -155,10 +153,6 @@ public class SignalAccount implements Closeable {
|
||||
private final KeyValueEntry<Long> lastReceiveTimestamp = new KeyValueEntry<>("last-receive-timestamp",
|
||||
long.class,
|
||||
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",
|
||||
Boolean.class,
|
||||
true);
|
||||
@ -198,10 +192,6 @@ public class SignalAccount implements Closeable {
|
||||
this.lock = lock;
|
||||
}
|
||||
|
||||
public File getDataPath() {
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
public static SignalAccount load(
|
||||
File dataPath,
|
||||
String accountPath,
|
||||
@ -234,28 +224,6 @@ public class SignalAccount implements Closeable {
|
||||
IdentityKeyPair pniIdentityKey,
|
||||
ProfileKey profileKey,
|
||||
final Settings settings
|
||||
) throws IOException {
|
||||
return create(dataPath,
|
||||
accountPath,
|
||||
number,
|
||||
null,
|
||||
serviceEnvironment,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
profileKey,
|
||||
settings);
|
||||
}
|
||||
|
||||
public static SignalAccount create(
|
||||
File dataPath,
|
||||
String accountPath,
|
||||
String number,
|
||||
ACI aci,
|
||||
ServiceEnvironment serviceEnvironment,
|
||||
IdentityKeyPair aciIdentityKey,
|
||||
IdentityKeyPair pniIdentityKey,
|
||||
ProfileKey profileKey,
|
||||
final Settings settings
|
||||
) throws IOException {
|
||||
IOUtils.createPrivateDirectories(dataPath);
|
||||
var fileName = getFileName(dataPath, accountPath);
|
||||
@ -274,7 +242,6 @@ public class SignalAccount implements Closeable {
|
||||
signalAccount.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
|
||||
signalAccount.dataPath = dataPath;
|
||||
signalAccount.aciAccountData.setServiceId(aci);
|
||||
signalAccount.aciAccountData.setIdentityKeyPair(aciIdentityKey);
|
||||
signalAccount.pniAccountData.setIdentityKeyPair(pniIdentityKey);
|
||||
signalAccount.aciAccountData.setLocalRegistrationId(KeyHelper.generateRegistrationId(false));
|
||||
@ -321,12 +288,11 @@ public class SignalAccount implements Closeable {
|
||||
final ACI aci,
|
||||
final PNI pni,
|
||||
final String password,
|
||||
final byte[] encryptedDeviceName,
|
||||
final String encryptedDeviceName,
|
||||
final IdentityKeyPair aciIdentity,
|
||||
final IdentityKeyPair pniIdentity,
|
||||
final ProfileKey profileKey,
|
||||
final AccountEntropyPool accountEntropyPool,
|
||||
final byte[] authCredentialSalt,
|
||||
final MediaRootBackupKey mediaRootBackupKey
|
||||
) {
|
||||
this.deviceId = 0;
|
||||
@ -337,13 +303,12 @@ public class SignalAccount implements Closeable {
|
||||
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
||||
this.password = password;
|
||||
this.profileKey = profileKey;
|
||||
this.encryptedDeviceName = org.signal.core.util.Base64.encodeWithoutPadding(encryptedDeviceName);
|
||||
this.encryptedDeviceName = encryptedDeviceName;
|
||||
this.aciAccountData.setIdentityKeyPair(aciIdentity);
|
||||
this.pniAccountData.setIdentityKeyPair(pniIdentity);
|
||||
this.registered = false;
|
||||
this.isMultiDevice = true;
|
||||
setLastReceiveTimestamp(0L);
|
||||
setLastAppliedPniChangeServerTimestamp(0L);
|
||||
if (accountEntropyPool != null) {
|
||||
this.pinMasterKey = null;
|
||||
this.accountEntropyPool = accountEntropyPool;
|
||||
@ -351,7 +316,6 @@ public class SignalAccount implements Closeable {
|
||||
this.pinMasterKey = null;
|
||||
this.accountEntropyPool = null;
|
||||
}
|
||||
this.authCredentialSalt = authCredentialSalt;
|
||||
this.mediaRootBackupKey = mediaRootBackupKey;
|
||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||
this.setStorageManifest(null);
|
||||
@ -374,9 +338,7 @@ public class SignalAccount implements Closeable {
|
||||
this.registered = true;
|
||||
this.deviceId = deviceId;
|
||||
setPreKeys(ServiceIdType.ACI, aciPreKeys);
|
||||
if (pniPreKeys != null) {
|
||||
setPreKeys(ServiceIdType.PNI, pniPreKeys);
|
||||
}
|
||||
setPreKeys(ServiceIdType.PNI, pniPreKeys);
|
||||
save();
|
||||
}
|
||||
|
||||
@ -390,7 +352,6 @@ public class SignalAccount implements Closeable {
|
||||
) {
|
||||
this.pinMasterKey = masterKey;
|
||||
this.accountEntropyPool = null;
|
||||
this.authCredentialSalt = null;
|
||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||
this.setStorageManifest(null);
|
||||
this.storageKey = null;
|
||||
@ -403,7 +364,6 @@ public class SignalAccount implements Closeable {
|
||||
init();
|
||||
this.registrationLockPin = pin;
|
||||
setLastReceiveTimestamp(0L);
|
||||
setLastAppliedPniChangeServerTimestamp(0L);
|
||||
save();
|
||||
|
||||
setPreKeys(ServiceIdType.ACI, aciPreKeys);
|
||||
@ -418,53 +378,6 @@ public class SignalAccount implements Closeable {
|
||||
clearSessionId();
|
||||
}
|
||||
|
||||
public void finishRecoveryRegistration(
|
||||
final ACI aci,
|
||||
final PNI pni,
|
||||
final String number,
|
||||
final AccountEntropyPool accountEntropyPool,
|
||||
final byte[] authCredentialSalt,
|
||||
final PreKeyCollection aciPreKeys,
|
||||
final PreKeyCollection pniPreKeys
|
||||
) {
|
||||
this.pinMasterKey = null;
|
||||
this.accountEntropyPool = accountEntropyPool;
|
||||
this.authCredentialSalt = authCredentialSalt;
|
||||
this.number = number;
|
||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||
this.setStorageManifest(null);
|
||||
this.storageKey = null;
|
||||
this.encryptedDeviceName = null;
|
||||
this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
this.isMultiDevice = false;
|
||||
this.registered = true;
|
||||
this.aciAccountData.setServiceId(aci);
|
||||
this.pniAccountData.setServiceId(pni);
|
||||
if (pni == null) {
|
||||
this.pniAccountData.setIdentityKeyPair(null);
|
||||
}
|
||||
init();
|
||||
this.registrationLockPin = null;
|
||||
setLastReceiveTimestamp(0L);
|
||||
setLastAppliedPniChangeServerTimestamp(0L);
|
||||
save();
|
||||
|
||||
setPreKeys(ServiceIdType.ACI, aciPreKeys);
|
||||
if (pni != null && pniPreKeys != null) {
|
||||
setPreKeys(ServiceIdType.PNI, pniPreKeys);
|
||||
}
|
||||
aciAccountData.getSessionStore().archiveAllSessions();
|
||||
pniAccountData.getSessionStore().archiveAllSessions();
|
||||
getSenderKeyStore().deleteAll();
|
||||
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
||||
trustSelfIdentity(ServiceIdType.ACI);
|
||||
if (pni != null) {
|
||||
trustSelfIdentity(ServiceIdType.PNI);
|
||||
}
|
||||
getKeyValueStore().storeEntry(lastRecipientsRefresh, null);
|
||||
clearSessionId();
|
||||
}
|
||||
|
||||
public void initDatabase() {
|
||||
getAccountDatabase();
|
||||
}
|
||||
@ -474,7 +387,7 @@ public class SignalAccount implements Closeable {
|
||||
}
|
||||
|
||||
private void migrateLegacyConfigs() {
|
||||
if (isPrimaryDevice() && (number != null || getPni() != null) && getPniIdentityKeyPair() == null) {
|
||||
if (isPrimaryDevice() && getPniIdentityKeyPair() == null) {
|
||||
logger.trace("Migrating legacy parts of account file");
|
||||
setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
|
||||
}
|
||||
@ -603,9 +516,6 @@ public class SignalAccount implements Closeable {
|
||||
if (storage.accountEntropyPool != null) {
|
||||
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
||||
}
|
||||
if (storage.authCredentialSalt != null) {
|
||||
authCredentialSalt = base64.decode(storage.authCredentialSalt);
|
||||
}
|
||||
if (storage.mediaRootBackupKey != null) {
|
||||
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
||||
}
|
||||
@ -986,7 +896,6 @@ public class SignalAccount implements Closeable {
|
||||
0,
|
||||
false,
|
||||
contact.blocked,
|
||||
0,
|
||||
contact.archived,
|
||||
false,
|
||||
false,
|
||||
@ -1095,7 +1004,6 @@ public class SignalAccount implements Closeable {
|
||||
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
||||
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
||||
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
||||
authCredentialSalt == null ? null : base64.encodeToString(authCredentialSalt),
|
||||
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
||||
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
||||
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
||||
@ -1310,11 +1218,6 @@ public class SignalAccount implements Closeable {
|
||||
return pniAccountData.getSignalServiceAccountDataStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignalServiceAccountDataStore pniOrNull() {
|
||||
return getPni() != null ? pniAccountData.getSignalServiceAccountDataStore() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMultiDevice() {
|
||||
return SignalAccount.this.isMultiDevice();
|
||||
@ -1496,56 +1399,7 @@ public class SignalAccount implements Closeable {
|
||||
}
|
||||
|
||||
public AccountAttributes.Capabilities getAccountCapabilities() {
|
||||
return getCapabilities(isPrimaryDevice(), number != null);
|
||||
}
|
||||
|
||||
public RegistrationApiV2.AccountAttributes getAccountAttributesV2() {
|
||||
return getAccountAttributesV2(false, getRegistrationLock());
|
||||
}
|
||||
|
||||
public RegistrationApiV2.AccountAttributes getAccountAttributesV2(
|
||||
final boolean includePniRegistrationId,
|
||||
final String registrationLock
|
||||
) {
|
||||
return getAccountAttributesV2(includePniRegistrationId,
|
||||
registrationLock,
|
||||
getRecoveryPassword(),
|
||||
number == null ? null : isDiscoverableByPhoneNumber());
|
||||
}
|
||||
|
||||
public RegistrationApiV2.AccountAttributes getAccountAttributesV2ForRecovery(
|
||||
final String registrationLock,
|
||||
final String recoveryPassword
|
||||
) {
|
||||
return getAccountAttributesV2(true, registrationLock, recoveryPassword, null);
|
||||
}
|
||||
|
||||
private RegistrationApiV2.AccountAttributes getAccountAttributesV2(
|
||||
final boolean includePniRegistrationId,
|
||||
final String registrationLock,
|
||||
final String recoveryPassword,
|
||||
final Boolean discoverableByPhoneNumber
|
||||
) {
|
||||
final var attributes = getAccountAttributes(null);
|
||||
final var capabilities = attributes.getCapabilities();
|
||||
return new RegistrationApiV2.AccountAttributes(attributes.getSignalingKey(),
|
||||
attributes.getRegistrationId(),
|
||||
attributes.getVoice(),
|
||||
attributes.getVideo(),
|
||||
attributes.getFetchesMessages(),
|
||||
registrationLock,
|
||||
attributes.getUnidentifiedAccessKey(),
|
||||
attributes.getUnrestrictedUnidentifiedAccess(),
|
||||
discoverableByPhoneNumber,
|
||||
new RegistrationApiV2.AccountAttributes.Capabilities(capabilities.getStorage(),
|
||||
capabilities.getVersionedExpirationTimer(),
|
||||
capabilities.getAttachmentBackfill(),
|
||||
capabilities.getSpqr(),
|
||||
capabilities.getUsernameChangeSyncMessage(),
|
||||
capabilities.getOptionalPhoneNumber()),
|
||||
attributes.getName(),
|
||||
includePniRegistrationId || getPni() != null ? attributes.getPniRegistrationId() : null,
|
||||
recoveryPassword);
|
||||
return getCapabilities(isPrimaryDevice());
|
||||
}
|
||||
|
||||
public ServiceId getAccountId(ServiceIdType serviceIdType) {
|
||||
@ -1758,10 +1612,6 @@ public class SignalAccount implements Closeable {
|
||||
return accountEntropyPool;
|
||||
}
|
||||
|
||||
public AccountEntropyPool getAccountEntropyPool() {
|
||||
return accountEntropyPool;
|
||||
}
|
||||
|
||||
public void setAccountEntropyPool(final AccountEntropyPool accountEntropyPool) {
|
||||
this.accountEntropyPool = accountEntropyPool;
|
||||
if (accountEntropyPool != null) {
|
||||
@ -1788,10 +1638,6 @@ public class SignalAccount implements Closeable {
|
||||
save();
|
||||
}
|
||||
|
||||
public byte[] getAuthCredentialSalt() {
|
||||
return authCredentialSalt;
|
||||
}
|
||||
|
||||
public String getRecoveryPassword() {
|
||||
final var masterKey = getPinBackedMasterKey();
|
||||
if (masterKey == null) {
|
||||
@ -1875,7 +1721,7 @@ public class SignalAccount implements Closeable {
|
||||
}
|
||||
|
||||
public boolean isRegistered() {
|
||||
return registered && deviceId > 0;
|
||||
return registered;
|
||||
}
|
||||
|
||||
public void setRegistered(final boolean registered) {
|
||||
@ -1903,14 +1749,6 @@ public class SignalAccount implements Closeable {
|
||||
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) {
|
||||
getKeyValueStore().storeEntry(needsToRetryFailedMessages, value);
|
||||
}
|
||||
@ -2082,8 +1920,7 @@ public class SignalAccount implements Closeable {
|
||||
getSessionStore(),
|
||||
getIdentityKeyStore(),
|
||||
getSenderKeyStore(),
|
||||
SignalAccount.this::isMultiDevice,
|
||||
SignalAccount.this::setMultiDevice));
|
||||
SignalAccount.this::isMultiDevice));
|
||||
}
|
||||
|
||||
public PreKeyStore getPreKeyStore() {
|
||||
@ -2131,7 +1968,6 @@ public class SignalAccount implements Closeable {
|
||||
String pinMasterKey,
|
||||
String storageKey,
|
||||
String accountEntropyPool,
|
||||
String authCredentialSalt,
|
||||
String mediaRootBackupKey,
|
||||
String profileKey,
|
||||
String usernameLinkEntropy,
|
||||
|
||||
@ -63,14 +63,11 @@ public class AccountsStore {
|
||||
public synchronized Set<AccountsStorage.Account> getAllAccounts() throws IOException {
|
||||
return readAccounts().stream()
|
||||
.filter(a -> a.environment() == null || serviceEnvironment.equals(a.environment()))
|
||||
.filter(a -> a.number() != null || a.uuid() != null)
|
||||
.filter(a -> a.number() != null)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public synchronized String getPathByNumber(String number) throws IOException {
|
||||
if (number == null) {
|
||||
return null;
|
||||
}
|
||||
return readAccounts().stream()
|
||||
.filter(a -> a.environment() == null || serviceEnvironment.equals(a.environment()))
|
||||
.filter(a -> number.equals(a.number()))
|
||||
@ -104,7 +101,7 @@ public class AccountsStore {
|
||||
if (number != null && number.equals(a.number())) {
|
||||
return new AccountsStorage.Account(a.path(), a.environment(), null, a.uuid());
|
||||
}
|
||||
if (aci != null && aci.toString().equals(a.uuid())) {
|
||||
if (aci != null && aci.toString().equals(a.toString())) {
|
||||
return new AccountsStorage.Account(a.path(), a.environment(), a.number(), null);
|
||||
}
|
||||
|
||||
@ -191,7 +188,7 @@ public class AccountsStore {
|
||||
private List<AccountsStorage.Account> readAccounts() throws IOException {
|
||||
final var pair = openFileChannel(getAccountsFile());
|
||||
try (final var fileChannel = pair.first(); final var lock = pair.second()) {
|
||||
var storage = readAccountsLocked(fileChannel);
|
||||
final var storage = readAccountsLocked(fileChannel);
|
||||
|
||||
var accountsVersion = storage.version() == null ? 1 : storage.version();
|
||||
if (accountsVersion > CURRENT_STORAGE_VERSION) {
|
||||
@ -200,7 +197,7 @@ public class AccountsStore {
|
||||
throw new IOException("Accounts file was created by a no longer supported older version: "
|
||||
+ accountsVersion);
|
||||
} else if (accountsVersion < CURRENT_STORAGE_VERSION) {
|
||||
storage = upgradeAccountsFile(fileChannel, storage, accountsVersion);
|
||||
return upgradeAccountsFile(fileChannel, storage, accountsVersion).accounts();
|
||||
}
|
||||
return storage.accounts();
|
||||
}
|
||||
|
||||
@ -58,10 +58,6 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
||||
|
||||
public abstract void setBlocked(boolean blocked);
|
||||
|
||||
public abstract long getBlockedAt();
|
||||
|
||||
public abstract void setBlockedAt(long blockedAt);
|
||||
|
||||
public abstract boolean isProfileSharingEnabled();
|
||||
|
||||
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 isTerminated();
|
||||
|
||||
public abstract GroupPermission getPermissionAddMember();
|
||||
|
||||
public abstract GroupPermission getPermissionEditDetails();
|
||||
|
||||
@ -24,7 +24,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
||||
public String color;
|
||||
public int messageExpirationTime;
|
||||
public boolean blocked;
|
||||
private long blockedAt;
|
||||
public boolean archived;
|
||||
private byte[] storageRecord;
|
||||
|
||||
@ -40,7 +39,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
||||
final String color,
|
||||
final int messageExpirationTime,
|
||||
final boolean blocked,
|
||||
final long blockedAt,
|
||||
final boolean archived,
|
||||
final byte[] storageRecord
|
||||
) {
|
||||
@ -51,7 +49,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
||||
this.color = color;
|
||||
this.messageExpirationTime = messageExpirationTime;
|
||||
this.blocked = blocked;
|
||||
this.blockedAt = blockedAt;
|
||||
this.archived = archived;
|
||||
this.storageRecord = storageRecord;
|
||||
}
|
||||
@ -94,24 +91,9 @@ public final class GroupInfoV1 extends GroupInfo {
|
||||
|
||||
@Override
|
||||
public void setBlocked(final boolean blocked) {
|
||||
if (blocked && !this.blocked) {
|
||||
blockedAt = System.currentTimeMillis();
|
||||
} else if (!blocked) {
|
||||
blockedAt = 0;
|
||||
}
|
||||
this.blocked = blocked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBlockedAt() {
|
||||
return blockedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlockedAt(final long blockedAt) {
|
||||
this.blockedAt = blockedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProfileSharingEnabled() {
|
||||
return true;
|
||||
@ -131,11 +113,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupPermission getPermissionAddMember() {
|
||||
return GroupPermission.EVERY_MEMBER;
|
||||
|
||||
@ -23,7 +23,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||
private final GroupMasterKey masterKey;
|
||||
private final DistributionId distributionId;
|
||||
private boolean blocked;
|
||||
private long blockedAt;
|
||||
private boolean profileSharingEnabled;
|
||||
private DecryptedGroup group;
|
||||
private byte[] storageRecord;
|
||||
@ -48,7 +47,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||
final DecryptedGroup group,
|
||||
final DistributionId distributionId,
|
||||
final boolean blocked,
|
||||
final long blockedAt,
|
||||
final boolean profileSharingEnabled,
|
||||
final boolean permissionDenied,
|
||||
final byte[] storageRecord,
|
||||
@ -59,7 +57,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||
this.group = group;
|
||||
this.distributionId = distributionId;
|
||||
this.blocked = blocked;
|
||||
this.blockedAt = blockedAt;
|
||||
this.profileSharingEnabled = profileSharingEnabled;
|
||||
this.permissionDenied = permissionDenied;
|
||||
this.storageRecord = storageRecord;
|
||||
@ -189,24 +186,9 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||
|
||||
@Override
|
||||
public void setBlocked(final boolean blocked) {
|
||||
if (blocked && !this.blocked) {
|
||||
blockedAt = System.currentTimeMillis();
|
||||
} else if (!blocked) {
|
||||
blockedAt = 0;
|
||||
}
|
||||
this.blocked = blocked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBlockedAt() {
|
||||
return blockedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlockedAt(final long blockedAt) {
|
||||
this.blockedAt = blockedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProfileSharingEnabled() {
|
||||
return profileSharingEnabled;
|
||||
@ -229,11 +211,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||
return this.group != null && this.group.isAnnouncementGroup == EnabledState.ENABLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return this.group != null && Boolean.TRUE.equals(this.group.terminated);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupPermission getPermissionAddMember() {
|
||||
final var accessControl = getAccessControl();
|
||||
|
||||
@ -63,7 +63,6 @@ public class GroupStore {
|
||||
distribution_id BLOB UNIQUE NOT NULL,
|
||||
endorsement_expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||
permission_denied INTEGER NOT NULL DEFAULT FALSE
|
||||
) STRICT;
|
||||
@ -84,7 +83,6 @@ public class GroupStore {
|
||||
color TEXT,
|
||||
expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
||||
archived INTEGER NOT NULL DEFAULT FALSE
|
||||
) STRICT;
|
||||
CREATE TABLE group_v1_member (
|
||||
@ -154,7 +152,6 @@ public class GroupStore {
|
||||
statement.setBytes(2, groupId.serialize());
|
||||
final var result = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
||||
if (result.isEmpty()) {
|
||||
connection.commit();
|
||||
return;
|
||||
}
|
||||
internalId = result.get();
|
||||
@ -403,7 +400,6 @@ public class GroupStore {
|
||||
deleteGroup(connection, groupInfoV1.getGroupId());
|
||||
final var groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, recipientResolver);
|
||||
groupInfoV2.setBlocked(groupInfoV1.isBlocked());
|
||||
groupInfoV2.setBlockedAt(groupInfoV1.getBlockedAt());
|
||||
updateGroup(connection, groupInfoV2);
|
||||
logger.debug("Locally migrated group {} to group v2, id: {}",
|
||||
groupInfoV1.getGroupId().toBase64(),
|
||||
@ -617,9 +613,9 @@ public class GroupStore {
|
||||
}
|
||||
}
|
||||
final var sql = """
|
||||
INSERT INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, blocked_at, archived, storage_id)
|
||||
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
|
||||
INSERT INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived, storage_id)
|
||||
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, archived=excluded.archived, storage_id=excluded.storage_id
|
||||
RETURNING _id
|
||||
""".formatted(TABLE_GROUP_V1);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
@ -634,9 +630,8 @@ public class GroupStore {
|
||||
statement.setString(5, groupV1.color);
|
||||
statement.setLong(6, groupV1.getMessageExpirationTimer());
|
||||
statement.setBoolean(7, groupV1.isBlocked());
|
||||
statement.setLong(8, groupV1.getBlockedAt());
|
||||
statement.setBoolean(9, groupV1.archived);
|
||||
statement.setBytes(10, KeyUtils.createRawStorageId());
|
||||
statement.setBoolean(8, groupV1.archived);
|
||||
statement.setBytes(9, KeyUtils.createRawStorageId());
|
||||
final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
||||
|
||||
if (internalId == null) {
|
||||
@ -662,9 +657,9 @@ public class GroupStore {
|
||||
} else if (group instanceof GroupInfoV2 groupV2) {
|
||||
final var sql = (
|
||||
"""
|
||||
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, blocked_at, permission_denied, storage_id, profile_sharing)
|
||||
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
|
||||
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, permission_denied, storage_id, profile_sharing)
|
||||
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, permission_denied=excluded.permission_denied, storage_id=excluded.storage_id, profile_sharing=excluded.profile_sharing
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V2);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
@ -682,10 +677,9 @@ public class GroupStore {
|
||||
}
|
||||
statement.setBytes(5, UuidUtil.toByteArray(groupV2.getDistributionId().asUuid()));
|
||||
statement.setBoolean(6, groupV2.isBlocked());
|
||||
statement.setLong(7, groupV2.getBlockedAt());
|
||||
statement.setBoolean(8, groupV2.isPermissionDenied());
|
||||
statement.setBytes(9, KeyUtils.createRawStorageId());
|
||||
statement.setBoolean(10, groupV2.isProfileSharingEnabled());
|
||||
statement.setBoolean(7, groupV2.isPermissionDenied());
|
||||
statement.setBytes(8, KeyUtils.createRawStorageId());
|
||||
statement.setBoolean(9, groupV2.isProfileSharingEnabled());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} else {
|
||||
@ -696,7 +690,7 @@ public class GroupStore {
|
||||
private List<GroupInfoV2> getGroupsV2() {
|
||||
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
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V2);
|
||||
@ -714,7 +708,7 @@ public class GroupStore {
|
||||
public GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
||||
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
|
||||
WHERE g.group_id = ?
|
||||
"""
|
||||
@ -748,7 +742,7 @@ public class GroupStore {
|
||||
public GroupInfoV2 getGroupV2(Connection connection, StorageId storageId) throws SQLException {
|
||||
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
|
||||
WHERE g.storage_id = ?
|
||||
"""
|
||||
@ -771,7 +765,6 @@ public class GroupStore {
|
||||
final var groupData = resultSet.getBytes("group_data");
|
||||
final var distributionId = resultSet.getBytes("distribution_id");
|
||||
final var blocked = resultSet.getBoolean("blocked");
|
||||
final var blockedAt = resultSet.getLong("blocked_at");
|
||||
final var profileSharingEnabled = resultSet.getBoolean("profile_sharing");
|
||||
final var permissionDenied = resultSet.getBoolean("permission_denied");
|
||||
final var storageRecord = resultSet.getBytes("storage_record");
|
||||
@ -780,7 +773,6 @@ public class GroupStore {
|
||||
groupData == null ? null : DecryptedGroup.ADAPTER.decode(groupData),
|
||||
DistributionId.from(UuidUtil.parseOrThrow(distributionId)),
|
||||
blocked,
|
||||
blockedAt,
|
||||
profileSharingEnabled,
|
||||
permissionDenied,
|
||||
storageRecord,
|
||||
@ -807,7 +799,7 @@ public class GroupStore {
|
||||
private List<GroupInfoV1> getGroupsV1() {
|
||||
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
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1);
|
||||
@ -825,7 +817,7 @@ public class GroupStore {
|
||||
public GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException {
|
||||
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
|
||||
WHERE g.group_id = ?
|
||||
"""
|
||||
@ -859,7 +851,7 @@ public class GroupStore {
|
||||
public GroupInfoV1 getGroupV1(Connection connection, StorageId storageId) throws SQLException {
|
||||
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
|
||||
WHERE g.storage_id = ?
|
||||
"""
|
||||
@ -889,7 +881,6 @@ public class GroupStore {
|
||||
.collect(Collectors.toSet());
|
||||
final var expirationTime = resultSet.getInt("expiration_time");
|
||||
final var blocked = resultSet.getBoolean("blocked");
|
||||
final var blockedAt = resultSet.getLong("blocked_at");
|
||||
final var archived = resultSet.getBoolean("archived");
|
||||
final var storageRecord = resultSet.getBytes("storage_record");
|
||||
return new GroupInfoV1(GroupId.v1(groupId),
|
||||
@ -899,7 +890,6 @@ public class GroupStore {
|
||||
color,
|
||||
expirationTime,
|
||||
blocked,
|
||||
blockedAt,
|
||||
archived,
|
||||
storageRecord);
|
||||
}
|
||||
@ -911,7 +901,7 @@ public class GroupStore {
|
||||
private GroupInfoV1 getGroupV1ByV2Id(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
||||
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
|
||||
WHERE g.group_id_v2 = ?
|
||||
"""
|
||||
|
||||
@ -59,7 +59,6 @@ public class LegacyGroupStore {
|
||||
g1.color,
|
||||
g1.messageExpirationTime,
|
||||
g1.blocked,
|
||||
0,
|
||||
g1.archived,
|
||||
null);
|
||||
}
|
||||
@ -78,7 +77,6 @@ public class LegacyGroupStore {
|
||||
loadDecryptedGroupLocked(groupId, groupCachePath),
|
||||
g2.distributionId == null ? DistributionId.create() : DistributionId.from(g2.distributionId),
|
||||
g2.blocked,
|
||||
0,
|
||||
true,
|
||||
g2.permissionDenied,
|
||||
null,
|
||||
|
||||
@ -26,7 +26,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
||||
@ -38,7 +37,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
||||
private final IdentityKeyStore identityKeyStore;
|
||||
private final SignalServiceSenderKeyStore senderKeyStore;
|
||||
private final Supplier<Boolean> isMultiDevice;
|
||||
private final Consumer<Boolean> setMultiDeviceCallback;
|
||||
|
||||
public SignalProtocolStore(
|
||||
final SignalServicePreKeyStore preKeyStore,
|
||||
@ -47,8 +45,7 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
||||
final SignalServiceSessionStore sessionStore,
|
||||
final IdentityKeyStore identityKeyStore,
|
||||
final SignalServiceSenderKeyStore senderKeyStore,
|
||||
final Supplier<Boolean> isMultiDevice,
|
||||
final Consumer<Boolean> setMultiDeviceCallback
|
||||
final Supplier<Boolean> isMultiDevice
|
||||
) {
|
||||
this.preKeyStore = preKeyStore;
|
||||
this.signedPreKeyStore = signedPreKeyStore;
|
||||
@ -57,7 +54,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
||||
this.identityKeyStore = identityKeyStore;
|
||||
this.senderKeyStore = senderKeyStore;
|
||||
this.isMultiDevice = isMultiDevice;
|
||||
this.setMultiDeviceCallback = setMultiDeviceCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -213,11 +209,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
||||
return isMultiDevice.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMultiDevice(final boolean isMultiDevice) {
|
||||
setMultiDeviceCallback.accept(isMultiDevice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KyberPreKeyRecord loadKyberPreKey(final int kyberPreKeyId) throws InvalidKeyIdException {
|
||||
return kyberPreKeyStore.loadKyberPreKey(kyberPreKeyId);
|
||||
|
||||
@ -50,7 +50,6 @@ public class LegacyRecipientStore2 {
|
||||
0,
|
||||
false,
|
||||
r.contact.blocked,
|
||||
0,
|
||||
r.contact.archived,
|
||||
r.contact.profileSharingEnabled,
|
||||
false,
|
||||
@ -101,7 +100,6 @@ public class LegacyRecipientStore2 {
|
||||
profile,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null);
|
||||
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
|
||||
|
||||
|
||||
@ -25,8 +25,6 @@ public class Recipient {
|
||||
|
||||
private final Long unregisteredTimestamp;
|
||||
|
||||
private final boolean pniSignatureVerified;
|
||||
|
||||
private final byte[] storageRecord;
|
||||
|
||||
public Recipient(
|
||||
@ -38,7 +36,6 @@ public class Recipient {
|
||||
final Profile profile,
|
||||
final Boolean discoverable,
|
||||
final Long unregisteredTimestamp,
|
||||
final boolean pniSignatureVerified,
|
||||
final byte[] storageRecord
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
@ -49,7 +46,6 @@ public class Recipient {
|
||||
this.profile = profile;
|
||||
this.discoverable = discoverable;
|
||||
this.unregisteredTimestamp = unregisteredTimestamp;
|
||||
this.pniSignatureVerified = pniSignatureVerified;
|
||||
this.storageRecord = storageRecord;
|
||||
}
|
||||
|
||||
@ -62,7 +58,6 @@ public class Recipient {
|
||||
profile = builder.profile;
|
||||
discoverable = builder.discoverable;
|
||||
unregisteredTimestamp = builder.unregisteredTimestamp;
|
||||
pniSignatureVerified = builder.pniSignatureVerified;
|
||||
storageRecord = builder.storageRecord;
|
||||
}
|
||||
|
||||
@ -78,9 +73,6 @@ public class Recipient {
|
||||
builder.profileKey = copy.getProfileKey();
|
||||
builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
|
||||
builder.profile = copy.getProfile();
|
||||
builder.discoverable = copy.getDiscoverable();
|
||||
builder.unregisteredTimestamp = copy.getUnregisteredTimestamp();
|
||||
builder.pniSignatureVerified = copy.isPniSignatureVerified();
|
||||
builder.storageRecord = copy.getStorageRecord();
|
||||
return builder;
|
||||
}
|
||||
@ -121,10 +113,6 @@ public class Recipient {
|
||||
return unregisteredTimestamp == null;
|
||||
}
|
||||
|
||||
public boolean isPniSignatureVerified() {
|
||||
return pniSignatureVerified;
|
||||
}
|
||||
|
||||
public byte[] getStorageRecord() {
|
||||
return storageRecord;
|
||||
}
|
||||
@ -139,19 +127,12 @@ public class Recipient {
|
||||
&& Objects.equals(contact, recipient.contact)
|
||||
&& Objects.equals(profileKey, recipient.profileKey)
|
||||
&& Objects.equals(expiringProfileKeyCredential, recipient.expiringProfileKeyCredential)
|
||||
&& Objects.equals(profile, recipient.profile)
|
||||
&& pniSignatureVerified == recipient.pniSignatureVerified;
|
||||
&& Objects.equals(profile, recipient.profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipientId,
|
||||
address,
|
||||
contact,
|
||||
profileKey,
|
||||
expiringProfileKeyCredential,
|
||||
profile,
|
||||
pniSignatureVerified);
|
||||
return Objects.hash(recipientId, address, contact, profileKey, expiringProfileKeyCredential, profile);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
@ -164,7 +145,6 @@ public class Recipient {
|
||||
private Profile profile;
|
||||
private Boolean discoverable;
|
||||
private Long unregisteredTimestamp;
|
||||
private boolean pniSignatureVerified;
|
||||
private byte[] storageRecord;
|
||||
|
||||
private Builder() {
|
||||
@ -210,11 +190,6 @@ public class Recipient {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withPniSignatureVerified(final boolean val) {
|
||||
pniSignatureVerified = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withStorageRecord(final byte[] val) {
|
||||
storageRecord = val;
|
||||
return this;
|
||||
|
||||
@ -28,9 +28,7 @@ import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@ -52,18 +50,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
|
||||
private final Map<Long, Long> recipientsMerged = new HashMap<>();
|
||||
|
||||
private static final int MAX_RECIPIENT_CACHE_SIZE = 2000;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = new HashMap<>();
|
||||
|
||||
public static void createSql(Connection connection) throws SQLException {
|
||||
// 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_credential BLOB,
|
||||
needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
|
||||
pni_signature_verified INTEGER NOT NULL DEFAULT FALSE,
|
||||
|
||||
given_name TEXT,
|
||||
family_name TEXT,
|
||||
@ -96,7 +82,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
expiration_time_version INTEGER DEFAULT 1 NOT NULL,
|
||||
mute_until INTEGER NOT NULL DEFAULT 0,
|
||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
||||
archived INTEGER NOT NULL DEFAULT FALSE,
|
||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||
hide_story INTEGER NOT NULL DEFAULT FALSE,
|
||||
@ -199,12 +184,12 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
|
||||
@Override
|
||||
public RecipientId resolveRecipient(final ServiceId serviceId) {
|
||||
final var recipientWithAddress = recipientAddressCache.get(serviceId);
|
||||
if (recipientWithAddress != null) {
|
||||
return recipientWithAddress.id();
|
||||
}
|
||||
try (final var connection = database.getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
final var recipientWithAddress = recipientAddressCache.get(serviceId);
|
||||
if (recipientWithAddress != null) {
|
||||
return recipientWithAddress.id();
|
||||
}
|
||||
final var recipientId = resolveRecipientLocked(connection, serviceId);
|
||||
connection.commit();
|
||||
return recipientId;
|
||||
@ -352,7 +337,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
public List<Pair<RecipientId, Contact>> getContacts() {
|
||||
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
|
||||
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,
|
||||
r.number, r.aci, r.pni, r.username,
|
||||
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.blocked_at, 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.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.discoverable,
|
||||
r.storage_record
|
||||
@ -397,8 +381,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
SELECT r._id,
|
||||
r.number, r.aci, r.pni, r.username,
|
||||
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.blocked_at, 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.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.discoverable,
|
||||
r.storage_record
|
||||
@ -447,8 +430,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
SELECT r._id,
|
||||
r.number, r.aci, r.pni, r.username,
|
||||
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.blocked_at, 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.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.discoverable,
|
||||
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() {
|
||||
final var sql = (
|
||||
"""
|
||||
@ -893,7 +841,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
final var sql = (
|
||||
"""
|
||||
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 = ?
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
@ -908,52 +856,28 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
statement.setBoolean(8, contact != null && contact.isProfileSharingEnabled());
|
||||
statement.setString(9, contact == null ? null : contact.color());
|
||||
statement.setBoolean(10, contact != null && contact.isBlocked());
|
||||
statement.setLong(11, contact == null ? 0 : contact.blockedAt());
|
||||
statement.setBoolean(12, contact != null && contact.isArchived());
|
||||
statement.setBoolean(11, contact != null && contact.isArchived());
|
||||
if (contact == null || contact.unregisteredTimestamp() == null) {
|
||||
statement.setNull(13, Types.INTEGER);
|
||||
statement.setNull(12, Types.INTEGER);
|
||||
} else {
|
||||
statement.setLong(13, contact.unregisteredTimestamp());
|
||||
statement.setLong(12, contact.unregisteredTimestamp());
|
||||
}
|
||||
statement.setString(14, contact == null ? null : contact.nickNameGivenName());
|
||||
statement.setString(15, contact == null ? null : contact.nickNameFamilyName());
|
||||
statement.setString(16, contact == null ? null : contact.note());
|
||||
statement.setBoolean(17, contact != null && contact.isHidden());
|
||||
statement.setLong(18, recipientId.id());
|
||||
statement.setString(13, contact == null ? null : contact.nickNameGivenName());
|
||||
statement.setString(14, contact == null ? null : contact.nickNameFamilyName());
|
||||
statement.setString(15, contact == null ? null : contact.note());
|
||||
statement.setBoolean(16, contact != null && contact.isHidden());
|
||||
statement.setLong(17, recipientId.id());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
if (contact != null && contact.unregisteredTimestamp() != null) {
|
||||
markUnregisteredAndSplitIfNecessary(connection, recipientId, contact.unregisteredTimestamp());
|
||||
markUnregisteredAndSplitIfNecessary(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(
|
||||
final Connection connection,
|
||||
final Collection<StorageId> storageIds
|
||||
final List<StorageId> storageIds
|
||||
) throws SQLException {
|
||||
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) {
|
||||
try (final var connection = database.getConnection()) {
|
||||
final var sql = (
|
||||
@ -1086,7 +947,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
if (recipientAddress.get().address().aci().isEmpty() || (
|
||||
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) {
|
||||
markRegistered(connection, recipientId);
|
||||
} else {
|
||||
markUnregisteredAndSplitIfNecessary(connection, recipientId, System.currentTimeMillis());
|
||||
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
@ -1130,10 +991,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
|
||||
private void markUnregisteredAndSplitIfNecessary(
|
||||
final Connection connection,
|
||||
final RecipientId recipientId,
|
||||
final long unregisteredTimestamp
|
||||
final RecipientId recipientId
|
||||
) throws SQLException {
|
||||
markUnregistered(connection, recipientId, unregisteredTimestamp);
|
||||
markUnregistered(connection, recipientId);
|
||||
final var address = resolveRecipientAddress(connection, recipientId);
|
||||
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
||||
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
||||
@ -1180,11 +1040,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
}
|
||||
}
|
||||
|
||||
private void markUnregistered(
|
||||
final Connection connection,
|
||||
final RecipientId recipientId,
|
||||
final long unregisteredTimestamp
|
||||
) throws SQLException {
|
||||
private void markUnregistered(final Connection connection, final RecipientId recipientId) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
@ -1193,7 +1049,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setLong(1, unregisteredTimestamp);
|
||||
statement.setLong(1, System.currentTimeMillis());
|
||||
statement.setLong(2, recipientId.id());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
@ -1354,9 +1210,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
final List<RecipientId> toBeMergedRecipientIds
|
||||
) throws SQLException {
|
||||
for (final var toBeMergedRecipientId : toBeMergedRecipientIds) {
|
||||
if (isPniSignatureVerified(connection, toBeMergedRecipientId)) {
|
||||
storePniSignatureVerified(connection, recipientId, true, true);
|
||||
}
|
||||
recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId);
|
||||
deleteRecipient(connection, toBeMergedRecipientId);
|
||||
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId));
|
||||
@ -1451,12 +1304,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET number = NULL,
|
||||
aci = NULL,
|
||||
pni = NULL,
|
||||
username = NULL,
|
||||
storage_id = NULL,
|
||||
pni_signature_verified = FALSE
|
||||
SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
|
||||
WHERE _id = ?
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
@ -1472,18 +1320,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
final RecipientAddress address
|
||||
) throws SQLException {
|
||||
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 = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET number = ?,
|
||||
aci = ?,
|
||||
pni = ?,
|
||||
username = ?,
|
||||
pni_signature_verified = ?
|
||||
SET number = ?, aci = ?, pni = ?, username = ?
|
||||
WHERE _id = ?
|
||||
"""
|
||||
).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(3, address.pni().map(PNI::toString).orElse(null));
|
||||
statement.setString(4, address.username().orElse(null));
|
||||
statement.setBoolean(5, keepPniSignatureVerified && isPniSignatureVerified(connection, recipientId));
|
||||
statement.setLong(6, recipientId.id());
|
||||
statement.setLong(5, recipientId.id());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
rotateStorageId(connection, recipientId);
|
||||
@ -1518,14 +1357,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
RecipientId toBeMergedRecipientId
|
||||
) throws SQLException {
|
||||
final var contact = getContact(connection, recipientId);
|
||||
final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
|
||||
if (contact == null) {
|
||||
final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
|
||||
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);
|
||||
@ -1550,24 +1384,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
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(
|
||||
final Connection connection,
|
||||
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 {
|
||||
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
|
||||
WHERE r._id = ? AND (%s)
|
||||
"""
|
||||
@ -1734,7 +1550,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
getProfileFromResultSet(resultSet),
|
||||
getDiscoverableFromResultSet(resultSet),
|
||||
getUnregisteredTimestampFromResultSet(resultSet),
|
||||
resultSet.getBoolean("pni_signature_verified"),
|
||||
getStorageRecordFromResultSet(resultSet));
|
||||
}
|
||||
|
||||
@ -1752,7 +1567,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
resultSet.getLong("mute_until"),
|
||||
resultSet.getBoolean("hide_story"),
|
||||
resultSet.getBoolean("blocked"),
|
||||
resultSet.getLong("blocked_at"),
|
||||
resultSet.getBoolean("archived"),
|
||||
resultSet.getBoolean("profile_sharing"),
|
||||
resultSet.getBoolean("hidden"),
|
||||
|
||||
@ -303,7 +303,6 @@ public class MessageSendLogStore implements AutoCloseable {
|
||||
}
|
||||
if (contentId == -1) {
|
||||
logger.warn("Failed to insert message send log content");
|
||||
connection.commit();
|
||||
return -1;
|
||||
}
|
||||
insertRecipientsForExistingContent(contentId, recipientDevices, connection);
|
||||
|
||||
@ -204,7 +204,7 @@ public class SenderKeySharedStore {
|
||||
).formatted(TABLE_SENDER_KEY_SHARED);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
for (final var entry : newEntries) {
|
||||
statement.setString(1, entry.address());
|
||||
statement.setString(1, entry.toString());
|
||||
statement.setInt(2, entry.deviceId());
|
||||
statement.setBytes(3, UuidUtil.toByteArray(distributionId.asUuid()));
|
||||
statement.setLong(4, System.currentTimeMillis());
|
||||
|
||||
@ -18,7 +18,7 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@ -28,15 +28,8 @@ public class SessionStore implements SignalServiceSessionStore {
|
||||
|
||||
private static final String TABLE_SESSION = "session";
|
||||
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) {
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Key, SessionRecord> eldest) {
|
||||
return size() > MAX_CACHE_SIZE;
|
||||
}
|
||||
};
|
||||
private final Map<Key, SessionRecord> cachedSessions = new HashMap<>();
|
||||
|
||||
private final Database database;
|
||||
private final int accountIdType;
|
||||
@ -201,9 +194,8 @@ public class SessionStore implements SignalServiceSessionStore {
|
||||
if (session != null) {
|
||||
session.archiveCurrentState();
|
||||
storeSession(connection, key, session);
|
||||
|
||||
connection.commit();
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update session store", e);
|
||||
}
|
||||
|
||||
@ -1,29 +1,10 @@
|
||||
package org.asamk.signal.manager.storage.stickers;
|
||||
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
|
||||
public record StickerPack(
|
||||
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 record StickerPack(long internalId, StickerPackId packId, byte[] packKey, boolean isInstalled) {
|
||||
|
||||
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.storage.Database;
|
||||
import org.asamk.signal.manager.storage.Utils;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.slf4j.Logger;
|
||||
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.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class StickerStore {
|
||||
|
||||
@ -32,11 +27,7 @@ public class StickerStore {
|
||||
_id INTEGER PRIMARY KEY,
|
||||
pack_id BLOB UNIQUE NOT NULL,
|
||||
pack_key BLOB NOT NULL,
|
||||
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
|
||||
installed INTEGER NOT NULL DEFAULT FALSE
|
||||
) STRICT;
|
||||
""");
|
||||
}
|
||||
@ -47,103 +38,55 @@ public class StickerStore {
|
||||
}
|
||||
|
||||
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 = (
|
||||
"""
|
||||
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
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
try (var result = Utils.executeQueryForStream(statement, this::getStickerPackFromResultSet)) {
|
||||
return result.toList();
|
||||
try (final var connection = database.getConnection()) {
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
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) {
|
||||
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 = (
|
||||
"""
|
||||
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
|
||||
WHERE s.pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, packId.serialize());
|
||||
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
public StickerPack getStickerPack(Connection connection, StorageId storageId) throws SQLException {
|
||||
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);
|
||||
try (final var connection = database.getConnection()) {
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, packId.serialize());
|
||||
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed read from sticker store", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addStickerPack(StickerPack stickerPack) {
|
||||
final var sql = (
|
||||
"""
|
||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO %s (pack_id, pack_key, installed)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
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)) {
|
||||
statement.setBytes(1, stickerPack.packId().serialize());
|
||||
statement.setBytes(2, stickerPack.packKey());
|
||||
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();
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update sticker store", e);
|
||||
}
|
||||
@ -153,279 +96,28 @@ public class StickerStore {
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?
|
||||
SET installed = ?
|
||||
WHERE pack_id = ?
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
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)) {
|
||||
statement.setBoolean(1, installed);
|
||||
statement.setInt(2, position);
|
||||
statement.setLong(3, deletedTimestamp);
|
||||
statement.setBytes(4, newStorageId.getRaw());
|
||||
statement.setBytes(5, stickerPackId.serialize());
|
||||
statement.setBytes(1, stickerPackId.serialize());
|
||||
statement.setBoolean(2, installed);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException 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) {
|
||||
logger.debug("Migrating legacy stickers to database");
|
||||
long start = System.nanoTime();
|
||||
final var sql = (
|
||||
"""
|
||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO %s (pack_id, pack_key, installed)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
).formatted(TABLE_STICKER);
|
||||
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))) {
|
||||
statement.executeUpdate();
|
||||
}
|
||||
var installedPosition = 0;
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
for (final var sticker : stickerPacks) {
|
||||
final var storageId = sticker.isInstalled()
|
||||
? StorageId.forStickerPack(KeyUtils.createRawStorageId())
|
||||
: null;
|
||||
statement.setBytes(1, sticker.packId().serialize());
|
||||
statement.setBytes(2, sticker.packKey());
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -465,36 +145,6 @@ public class StickerStore {
|
||||
final var packId = resultSet.getBytes("pack_id");
|
||||
final var packKey = resultSet.getBytes("pack_key");
|
||||
final var installed = resultSet.getBoolean("installed");
|
||||
final var position = resultSet.getInt("position");
|
||||
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"));
|
||||
}
|
||||
return new StickerPack(internalId, StickerPackId.deserialize(packId), packKey, installed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,10 +24,8 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import okio.ByteString;
|
||||
@ -48,69 +46,16 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
private final SignalAccount account;
|
||||
private final Connection connection;
|
||||
private final JobExecutor jobExecutor;
|
||||
private final Set<StorageId> identityConflictsPendingRepair;
|
||||
|
||||
public ContactRecordProcessor(
|
||||
SignalAccount account,
|
||||
Connection connection,
|
||||
final JobExecutor jobExecutor,
|
||||
final Set<StorageId> identityConflictsPendingRepair
|
||||
) {
|
||||
public ContactRecordProcessor(SignalAccount account, Connection connection, final JobExecutor jobExecutor) {
|
||||
this.account = account;
|
||||
this.connection = connection;
|
||||
this.jobExecutor = jobExecutor;
|
||||
this.identityConflictsPendingRepair = identityConflictsPendingRepair;
|
||||
this.selfAci = account.getAci();
|
||||
this.selfPni = account.getPni();
|
||||
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();
|
||||
}
|
||||
|
||||
static boolean shouldUseRemoteIdentityKey(
|
||||
final boolean isPrimaryDevice,
|
||||
final boolean statesDiffer,
|
||||
final int remoteIdentityKeySize,
|
||||
final int localIdentityKeySize,
|
||||
final long localUnregisteredAtTimestamp,
|
||||
final boolean unrepairableIdentityKeyConflict
|
||||
) {
|
||||
return remoteIdentityKeySize > 0 && (
|
||||
statesDiffer || localIdentityKeySize == 0 || localUnregisteredAtTimestamp > 0 || (
|
||||
unrepairableIdentityKeyConflict && !isPrimaryDevice
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String describeRecord(final SignalContactRecord record) {
|
||||
final var proto = record.getProto();
|
||||
final var aci = ACI.parseOrNull(proto.aci, proto.aciBinary);
|
||||
final var pni = PNI.parseOrNull(proto.pni, proto.pniBinary);
|
||||
return "[" + firstNonNull(aci, pni) + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Error cases:
|
||||
* - You can't have a contact record without an ACI or PNI.
|
||||
@ -172,40 +117,32 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
|
||||
IdentityState identityState;
|
||||
ByteString identityKey;
|
||||
if (remote.identityKey.size() > 0 && (
|
||||
!account.isPrimaryDevice()
|
||||
|| remote.identityState != local.identityState
|
||||
|| local.identityKey.size() == 0
|
||||
|
||||
)) {
|
||||
identityState = remote.identityState;
|
||||
identityKey = remote.identityKey;
|
||||
} else {
|
||||
identityState = local.identityState;
|
||||
identityKey = local.identityKey.size() > 0 ? local.identityKey : ByteString.EMPTY;
|
||||
}
|
||||
|
||||
// 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;
|
||||
final var identityKeysExistAndConflict = remoteIdentityKeySize > 0
|
||||
&& localIdentityKeySize > 0
|
||||
&& !remote.identityKey.equals(local.identityKey);
|
||||
final var conflictAci = firstNonNull(localAci, remoteAci);
|
||||
final var unrepairableIdentityKeyConflict = identityKeysExistAndConflict && conflictAci == null;
|
||||
|
||||
if (shouldUseRemoteIdentityKey(account.isPrimaryDevice(),
|
||||
statesDiffer,
|
||||
remoteIdentityKeySize,
|
||||
localIdentityKeySize,
|
||||
local.unregisteredAtTimestamp,
|
||||
unrepairableIdentityKeyConflict)) {
|
||||
identityState = remote.identityState;
|
||||
identityKey = remote.identityKey;
|
||||
} else {
|
||||
identityState = local.identityState;
|
||||
if (hasLocalIdentity && localIdentityKeySize > 0) {
|
||||
identityKey = local.identityKey;
|
||||
} else {
|
||||
identityKey = ByteString.EMPTY;
|
||||
}
|
||||
if (localAci != null
|
||||
&& local.identityKey.size() > 0
|
||||
&& remote.identityKey.size() > 0
|
||||
&& !local.identityKey.equals(remote.identityKey)) {
|
||||
logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
|
||||
localAci);
|
||||
final var address = getRecipientAddress(local);
|
||||
jobExecutor.enqueueJob(new DownloadProfileJob(address));
|
||||
}
|
||||
|
||||
PNI pni;
|
||||
@ -241,17 +178,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
e164 = firstNonEmpty(remote.e164, local.e164);
|
||||
}
|
||||
|
||||
if (identityKeysExistAndConflict) {
|
||||
if (conflictAci != null) {
|
||||
logger.debug("Identity keys conflict for {}. Enqueueing a profile fetch.", conflictAci);
|
||||
jobExecutor.enqueueJob(new DownloadProfileJob(new RecipientAddress(conflictAci, pni, e164), true));
|
||||
} else {
|
||||
logger.debug("Identity keys conflict for {}. No ACI, so no profile fetch is possible.", localPni);
|
||||
}
|
||||
} else if (identityKey.size() > 0 && remoteIdentityKeySize == 0) {
|
||||
logger.debug("Remote identity key is missing for {}. Keeping ours.", firstNonNull(localAci, localPni));
|
||||
}
|
||||
|
||||
final var remoteProfileKey = remote.profileKey.size() == 0
|
||||
|| KeyUtils.profileKeyOrNull(remote.profileKey.toByteArray()) == null
|
||||
? ByteString.EMPTY
|
||||
@ -268,7 +194,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
.identityState(identityState)
|
||||
.identityKey(identityKey)
|
||||
.blocked(remote.blocked)
|
||||
.blockedAtTimestamp(remote.blockedAtTimestamp)
|
||||
.whitelisted(remote.whitelisted)
|
||||
.archived(remote.archived)
|
||||
.markedUnread(remote.markedUnread)
|
||||
@ -276,9 +201,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
.hideStory(remote.hideStory)
|
||||
.unregisteredAtTimestamp(remote.unregisteredAtTimestamp)
|
||||
.hidden(remote.hidden)
|
||||
.pniSignatureVerified((remote.pniSignatureVerified || local.pniSignatureVerified)
|
||||
&& pni != null
|
||||
&& pni.isValid())
|
||||
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
|
||||
.nickname(remote.nickname)
|
||||
.note(remote.note)
|
||||
.avatarColor(remote.avatarColor);
|
||||
@ -313,9 +236,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
|
||||
final var matchesLocal = doProtosMatch(merged, local);
|
||||
if (matchesLocal) {
|
||||
if (identityKeysExistAndConflict && conflictAci != null) {
|
||||
identityConflictsPendingRepair.add(localRecord.getId());
|
||||
}
|
||||
return localRecord;
|
||||
}
|
||||
|
||||
@ -352,9 +272,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
|
||||
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
|
||||
final var contactNote = contact == null ? null : contact.note();
|
||||
final var blockedAt = contact == null ? 0 : contact.blockedAt();
|
||||
if (blocked != contactProto.blocked
|
||||
|| blockedAt != contactProto.blockedAtTimestamp
|
||||
|| profileShared != contactProto.whitelisted
|
||||
|| archived != contactProto.archived
|
||||
|| hidden != contactProto.hidden
|
||||
@ -372,7 +290,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
logger.debug("Storing new or updated contact {}", recipientId);
|
||||
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||
final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
|
||||
.withBlockedAt(contactProto.blocked ? contactProto.blockedAtTimestamp : 0)
|
||||
.withIsProfileSharingEnabled(contactProto.whitelisted)
|
||||
.withIsArchived(contactProto.archived)
|
||||
.withIsHidden(contactProto.hidden)
|
||||
@ -429,8 +346,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||
logger.warn("Received invalid contact identity key from storage");
|
||||
}
|
||||
}
|
||||
account.getRecipientStore()
|
||||
.storePniSignatureVerified(connection, recipientId, contactProto.pniSignatureVerified);
|
||||
account.getRecipientStore()
|
||||
.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 java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
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 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
|
||||
@ -53,7 +50,6 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
|
||||
|
||||
if (local.isEmpty()) {
|
||||
debug(remote.getId(), remote, "[Local Insert] No matching local record. Inserting.");
|
||||
updatedStorageIds.add(remote.getId());
|
||||
insertLocal(remote);
|
||||
return;
|
||||
}
|
||||
@ -68,7 +64,6 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
|
||||
matchedRecords.add(local.get());
|
||||
|
||||
final var merged = merge(remote, local.get());
|
||||
updatedStorageIds.add(merged.getId());
|
||||
if (!merged.equals(remote)) {
|
||||
debug(remote.getId(), remote, "[Remote Update] " + merged.describeDiff(remote));
|
||||
}
|
||||
@ -80,19 +75,8 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
|
||||
}
|
||||
}
|
||||
|
||||
public Set<StorageId> getUpdatedStorageIds() {
|
||||
return Collections.unmodifiableSet(updatedStorageIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional extra identifying detail about a record, included in every log line for it.
|
||||
*/
|
||||
protected String describeRecord(E record) {
|
||||
return "";
|
||||
}
|
||||
|
||||
private void debug(StorageId i, E record, String message) {
|
||||
logger.debug("[{}][{}]{} {}", i, record.getClass().getSimpleName(), describeRecord(record), 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);
|
||||
if (group != null) {
|
||||
group.setBlocked(groupV1Proto.blocked);
|
||||
group.setBlockedAt(0);
|
||||
account.getGroupStore().updateGroup(connection, group);
|
||||
account.getGroupStore()
|
||||
.storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
|
||||
|
||||
@ -56,7 +56,6 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
||||
final var mergedBuilder = remote.newBuilder()
|
||||
.masterKey(remote.masterKey)
|
||||
.blocked(remote.blocked)
|
||||
.blockedAtTimestamp(remote.blockedAtTimestamp)
|
||||
.whitelisted(remote.whitelisted)
|
||||
.archived(remote.archived)
|
||||
.markedUnread(remote.markedUnread)
|
||||
@ -94,7 +93,6 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
||||
|
||||
final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
|
||||
group.setBlocked(groupV2Proto.blocked);
|
||||
group.setBlockedAt(groupV2Proto.blocked ? groupV2Proto.blockedAtTimestamp : 0);
|
||||
group.setProfileSharingEnabled(groupV2Proto.whitelisted);
|
||||
account.getGroupStore().updateGroup(connection, group);
|
||||
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,151 +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());
|
||||
private boolean lastAttemptChargedContent;
|
||||
private boolean lastAttemptChargedRate;
|
||||
|
||||
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
|
||||
) {
|
||||
lastAttemptChargedContent = false;
|
||||
lastAttemptChargedRate = false;
|
||||
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);
|
||||
lastAttemptChargedContent = true;
|
||||
}
|
||||
if (fetchedRemoteManifest) {
|
||||
rateBucket.use(now);
|
||||
lastAttemptChargedRate = true;
|
||||
}
|
||||
if (fingerprint != null) {
|
||||
remember(fingerprint);
|
||||
}
|
||||
|
||||
return Decision.Allowed.INSTANCE;
|
||||
}
|
||||
|
||||
public synchronized void onWriteFailed() {
|
||||
onWriteFailed(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
synchronized void onWriteFailed(final long now) {
|
||||
if (lastAttemptChargedContent) {
|
||||
contentBucket.refund(now);
|
||||
lastAttemptChargedContent = false;
|
||||
}
|
||||
if (lastAttemptChargedRate) {
|
||||
rateBucket.refund(now);
|
||||
lastAttemptChargedRate = false;
|
||||
}
|
||||
}
|
||||
|
||||
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.identities.IdentityInfo;
|
||||
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.PNI;
|
||||
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.SignalGroupV1Record;
|
||||
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.UsernameLink;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StickerPackRecord;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
@ -95,13 +92,9 @@ public final class StorageSyncModels {
|
||||
|
||||
public static ContactRecord localToRemoteRecord(Recipient recipient, IdentityInfo identity) {
|
||||
final var address = recipient.getAddress();
|
||||
final var aciPresent = address.aci().isPresent();
|
||||
|
||||
final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
|
||||
.e164(address.number().orElse(""))
|
||||
.username(address.username().orElse(""))
|
||||
.pniSignatureVerified(address.pni().map(PNI::isValid).orElse(false)
|
||||
&& recipient.isPniSignatureVerified())
|
||||
.profileKey(recipient.getProfileKey() == null
|
||||
? ByteString.EMPTY
|
||||
: ByteString.of(recipient.getProfileKey().serialize()));
|
||||
@ -124,7 +117,6 @@ public final class StorageSyncModels {
|
||||
.nickname(getNicknameRemoteRecord(recipient.getContact()))
|
||||
.note(emptyIfNull(recipient.getContact().note()))
|
||||
.blocked(recipient.getContact().isBlocked())
|
||||
.blockedAtTimestamp(recipient.getContact().blockedAt())
|
||||
.whitelisted(recipient.getContact().isProfileSharingEnabled())
|
||||
.mutedUntilTimestamp(recipient.getContact().muteUntil())
|
||||
.hideStory(recipient.getContact().hideStory())
|
||||
@ -134,7 +126,7 @@ public final class StorageSyncModels {
|
||||
.archived(recipient.getContact().isArchived())
|
||||
.hidden(recipient.getContact().isHidden());
|
||||
}
|
||||
if (identity != null && aciPresent) {
|
||||
if (identity != null) {
|
||||
builder.identityKey(ByteString.of(identity.getIdentityKey().serialize()))
|
||||
.identityState(localToRemote(identity.getTrustLevel()));
|
||||
}
|
||||
@ -163,28 +155,10 @@ public final class StorageSyncModels {
|
||||
final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
|
||||
builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
|
||||
builder.blocked(group.isBlocked());
|
||||
builder.blockedAtTimestamp(group.getBlockedAt());
|
||||
builder.whitelisted(group.isProfileSharingEnabled());
|
||||
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) {
|
||||
return switch (identityState) {
|
||||
case DEFAULT -> TrustLevel.TRUSTED_UNVERIFIED;
|
||||
|
||||
@ -168,11 +168,6 @@ public final class StorageSyncValidations {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -222,8 +217,6 @@ public final class StorageSyncValidations {
|
||||
|
||||
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 DeletePresentInFullIdSetError extends Error {}
|
||||
|
||||
@ -1,50 +1,51 @@
|
||||
package org.asamk.signal.manager.util;
|
||||
|
||||
import org.asamk.signal.manager.api.Message.AttachmentDimensions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
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.
|
||||
private static final long MAX_DIMENSION_PROBE_SIZE = 20 * 1024 * 1024;
|
||||
return createAttachmentStream(streamDetails.first(), streamDetails.second(), voiceNote, resumableUploadSpec);
|
||||
} 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(
|
||||
StreamDetails streamDetails,
|
||||
Optional<String> name,
|
||||
boolean voiceNote,
|
||||
AttachmentDimensions dimensions,
|
||||
String blurHash,
|
||||
ResumableUploadSpec resumableUploadSpec
|
||||
) throws ResumeLocationInvalidException, IOException {
|
||||
) throws ResumeLocationInvalidException {
|
||||
final var uploadTimestamp = System.currentTimeMillis();
|
||||
final var probedStream = dimensions != null ? new ProbedStream(streamDetails.getStream(),
|
||||
dimensions.width(),
|
||||
dimensions.height()) : probeImageDimensions(streamDetails);
|
||||
return SignalServiceAttachmentStream.newStreamBuilder()
|
||||
.withStream(probedStream.inputStream())
|
||||
.withStream(streamDetails.getStream())
|
||||
.withContentType(streamDetails.getContentType())
|
||||
.withLength(streamDetails.getLength())
|
||||
.withFileName(name.orElse(null))
|
||||
.withVoiceNote(voiceNote)
|
||||
.withBlurHash(blurHash)
|
||||
.withWidth(probedStream.width())
|
||||
.withHeight(probedStream.height())
|
||||
.withUploadTimestamp(uploadTimestamp)
|
||||
.withResumableUploadSpec(resumableUploadSpec)
|
||||
.withUuid(UUID.randomUUID())
|
||||
@ -55,55 +56,7 @@ public class AttachmentUtils {
|
||||
StreamDetails streamDetails,
|
||||
Optional<String> name,
|
||||
ResumableUploadSpec resumableUploadSpec
|
||||
) throws ResumeLocationInvalidException, IOException {
|
||||
return createAttachmentStream(streamDetails, name, false, null, null, resumableUploadSpec);
|
||||
) throws ResumeLocationInvalidException {
|
||||
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 static File createTempFile() throws IOException {
|
||||
final var prefix = "signal-cli_tmp_";
|
||||
final var suffix = ".tmp";
|
||||
try {
|
||||
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;
|
||||
}
|
||||
final var tempFile = File.createTempFile("signal-cli_tmp_", ".tmp");
|
||||
tempFile.deleteOnExit();
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
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.helper.PinHelper;
|
||||
import org.signal.core.models.MasterKey;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException;
|
||||
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.registration.RegistrationApi;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
@ -65,12 +65,14 @@ public class NumberVerificationUtils {
|
||||
if (nextAttempt == null) {
|
||||
throw new VerificationMethodNotAvailableException();
|
||||
} 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();
|
||||
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")) {
|
||||
|
||||
@ -42,19 +42,6 @@ public class PhoneNumberFormatter {
|
||||
throw new InvalidNumberException("No valid characters found.");
|
||||
}
|
||||
|
||||
if (localNumber == null) {
|
||||
if (!number.startsWith("+")) {
|
||||
throw new InvalidNumberException(
|
||||
"Use an international number including the country code for a numberless account.");
|
||||
}
|
||||
try {
|
||||
final var util = PhoneNumberUtil.getInstance();
|
||||
return util.format(util.parse(number, null), PhoneNumberFormat.E164);
|
||||
} catch (NumberParseException e) {
|
||||
throw new InvalidNumberException("Invalid international phone number.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||
PhoneNumber localNumberObject = util.parse(localNumber, null);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package org.asamk.signal.manager.util;
|
||||
|
||||
import org.asamk.signal.manager.api.BadRequestException;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.signal.core.models.ServiceId;
|
||||
import org.signal.libsignal.net.BadRequestError;
|
||||
@ -8,9 +7,9 @@ import org.signal.libsignal.net.RequestResult;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
|
||||
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
|
||||
import org.signal.network.NetworkResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.NetworkResult;
|
||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
@ -18,6 +17,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
@ -39,9 +39,6 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import kotlin.coroutines.Continuation;
|
||||
import kotlin.coroutines.EmptyCoroutineContext;
|
||||
import kotlinx.coroutines.BuildersKt;
|
||||
import okio.ByteString;
|
||||
|
||||
public class Utils {
|
||||
@ -56,7 +53,7 @@ public class Utils {
|
||||
}
|
||||
|
||||
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 mime = MimeUtils.getFileMimeType(file).orElse(MimeUtils.OCTET_STREAM);
|
||||
return new StreamDetails(stream, mime, size);
|
||||
@ -164,24 +161,6 @@ public class Utils {
|
||||
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")
|
||||
public static <T, E extends BadRequestError> T handleResponseException(final RequestResult<T, E> result) throws IOException {
|
||||
if (result instanceof RequestResult.Success<?> success) {
|
||||
@ -195,8 +174,8 @@ public class Utils {
|
||||
}
|
||||
} else if (result instanceof RequestResult.RetryableNetworkError e) {
|
||||
throw e.getNetworkError();
|
||||
} else if (result instanceof RequestResult.NonSuccess<?> e) {
|
||||
throw new BadRequestException(e.getError());
|
||||
} else if (result instanceof RequestResult.NonSuccess) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
throw new IllegalStateException("Unexpected value: " + result);
|
||||
}
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.Settings;
|
||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.storage.protos.groups.local.DecryptedMember;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class NumberlessGroupTest {
|
||||
|
||||
@TempDir
|
||||
Path directory;
|
||||
|
||||
@Test
|
||||
void findsOurGroupMembershipByAciWhenPniIsAbsent() throws Exception {
|
||||
final var aci = ACI.parseOrThrow("11111111-1111-4111-8111-111111111111");
|
||||
final var otherAci = ACI.parseOrThrow("22222222-2222-4222-8222-222222222222");
|
||||
try (final var account = SignalAccount.createLinkedAccount(directory.toFile(),
|
||||
"account",
|
||||
ServiceEnvironment.STAGING,
|
||||
Settings.DEFAULT); final var context = new Context(account, null, null, null, null, null)) {
|
||||
account.setProvisioningData(null,
|
||||
aci,
|
||||
null,
|
||||
"test-password",
|
||||
new byte[]{1},
|
||||
KeyUtils.generateIdentityKeyPair(),
|
||||
null,
|
||||
KeyUtils.createProfileKey(),
|
||||
null,
|
||||
new byte[32],
|
||||
null);
|
||||
final var otherMember = new DecryptedMember.Builder().aciBytes(otherAci.toByteString())
|
||||
.joinedAtRevision(1)
|
||||
.build();
|
||||
final var selfMember = new DecryptedMember.Builder().aciBytes(aci.toByteString())
|
||||
.joinedAtRevision(4)
|
||||
.build();
|
||||
final var group = new DecryptedGroup.Builder().revision(9)
|
||||
.members(List.of(otherMember, selfMember))
|
||||
.build();
|
||||
assertEquals(4, context.getGroupV2Helper().findRevisionWeWereAdded(group));
|
||||
assertEquals(9,
|
||||
context.getGroupV2Helper()
|
||||
.findRevisionWeWereAdded(group.newBuilder().members(List.of(otherMember)).build()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.syncStorage.WriteOperationResult;
|
||||
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 java.util.Set;
|
||||
|
||||
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 StorageHelperTest {
|
||||
|
||||
@Test
|
||||
void removesKnownTypeLocalOnlyIdsStoredAsUnknown() {
|
||||
final var staleStickerPackId = StorageId.forStickerPack(new byte[]{1});
|
||||
final var localContactId = StorageId.forContact(new byte[]{2});
|
||||
|
||||
final var result = StorageHelper.findUnknownOnlyLocalStorageIds(List.of(staleStickerPackId, localContactId),
|
||||
Set.of(staleStickerPackId));
|
||||
|
||||
assertEquals(List.of(staleStickerPackId), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defersWritesContainingOnlyIdentityConflictsPendingRepair() {
|
||||
final var pendingId = StorageId.forContact(new byte[]{1});
|
||||
final var otherId = StorageId.forContact(new byte[]{2});
|
||||
final var pendingRecord = record(pendingId);
|
||||
final var otherRecord = record(otherId);
|
||||
|
||||
assertTrue(StorageHelper.containsOnlyIdentityConflictsPendingRepair(write(List.of(pendingRecord)),
|
||||
Set.of(pendingId)));
|
||||
assertFalse(StorageHelper.containsOnlyIdentityConflictsPendingRepair(write(List.of(pendingRecord, otherRecord)),
|
||||
Set.of(pendingId)));
|
||||
assertFalse(StorageHelper.containsOnlyIdentityConflictsPendingRepair(write(List.of()), Set.of(pendingId)));
|
||||
assertFalse(StorageHelper.containsOnlyIdentityConflictsPendingRepair(new WriteOperationResult(null,
|
||||
List.of(pendingRecord),
|
||||
List.of(new byte[]{3})), Set.of(pendingId)));
|
||||
}
|
||||
|
||||
private static SignalStorageRecord record(final StorageId id) {
|
||||
return new SignalStorageRecord(id, new StorageRecord.Builder().build());
|
||||
}
|
||||
|
||||
private static WriteOperationResult write(final List<SignalStorageRecord> inserts) {
|
||||
return new WriteOperationResult(null, inserts, List.of());
|
||||
}
|
||||
}
|
||||
@ -1,182 +0,0 @@
|
||||
package org.asamk.signal.manager.internal;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.asamk.signal.manager.Settings;
|
||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.signal.core.models.ServiceId.PNI;
|
||||
import org.signal.network.api.RegistrationApiV2;
|
||||
import org.signal.network.rest.SignalRestClient;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Protocol;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
import okio.ByteString;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class NumberlessProvisioningTest {
|
||||
|
||||
private static final ACI ACI_ID = ACI.parseOrThrow("11111111-1111-4111-8111-111111111111");
|
||||
private static final PNI PNI_ID = PNI.parseOrThrow("22222222-2222-4222-8222-222222222222");
|
||||
|
||||
@TempDir
|
||||
Path directory;
|
||||
|
||||
@Test
|
||||
void numberlessProvisioningRequiresGroupCredentialSalt() {
|
||||
assertThrows(IOException.class, () -> ProvisioningManagerImpl.parsePni(new ProvisionMessage.Builder().build()));
|
||||
final var emptySalt = new ProvisionMessage.Builder().authCredentialSalt(ByteString.EMPTY).build();
|
||||
assertThrows(IOException.class, () -> ProvisioningManagerImpl.parsePni(emptySalt));
|
||||
assertNull(ProvisioningManagerImpl.toRegistrationPreKeys(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsNumberlessProvisioningButRejectsInconsistentPhoneIdentity() throws Exception {
|
||||
final var message = new ProvisionMessage.Builder().authCredentialSalt(ByteString.of(new byte[32]));
|
||||
assertNull(ProvisioningManagerImpl.parsePni(message.build()));
|
||||
assertThrows(IOException.class, () -> ProvisioningManagerImpl.parsePni(message.pni(PNI_ID.toString()).build()));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> ProvisioningManagerImpl.parsePni(new ProvisionMessage.Builder().number("+12025550123").build()));
|
||||
assertEquals(PNI_ID,
|
||||
ProvisioningManagerImpl.parsePni(new ProvisionMessage.Builder().number("+12025550123")
|
||||
.pni(PNI_ID.toString())
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberlessLinkRequestOmitsPniAndAuthenticatesWithAci() throws Exception {
|
||||
checkLinkRequest(null, null, 200, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberedLinkRequestStillIncludesPniKeys() throws Exception {
|
||||
checkLinkRequest("+12025550123", PNI_ID, 200, null);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"422, Signal rejected the device linking request",
|
||||
"403, [403] Device verification failed",
|
||||
"409, Linked device is missing a required account capability",
|
||||
"411, Account has reached its linked device limit",
|
||||
"429, Device linking rate limited; try again later"
|
||||
})
|
||||
void linkingErrorsAreReportedWithoutCrashingOrIncludingServerBody(
|
||||
final int statusCode,
|
||||
final String errorMessage
|
||||
) throws Exception {
|
||||
checkLinkRequest(null, null, statusCode, errorMessage);
|
||||
}
|
||||
|
||||
private void checkLinkRequest(
|
||||
final String number,
|
||||
final PNI pni,
|
||||
final int statusCode,
|
||||
final String errorMessage
|
||||
) throws Exception {
|
||||
final var request = new AtomicReference<Request>();
|
||||
final var body = new AtomicReference<JsonNode>();
|
||||
final var mapper = new ObjectMapper();
|
||||
final var client = new OkHttpClient.Builder().addInterceptor(chain -> {
|
||||
request.set(chain.request());
|
||||
final var buffer = new Buffer();
|
||||
chain.request().body().writeTo(buffer);
|
||||
body.set(mapper.readTree(buffer.readUtf8()));
|
||||
return new Response.Builder().request(chain.request())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(statusCode)
|
||||
.message("Test response")
|
||||
.body(ResponseBody.create(statusCode == 200 ? "{\"deviceId\":2}" : "secret payload",
|
||||
MediaType.get("application/json")))
|
||||
.build();
|
||||
}).build();
|
||||
final var config = ServiceConfig.getServiceEnvironmentConfig(ServiceEnvironment.STAGING, "signal-cli-test");
|
||||
final var restClient = new SignalRestClient(config.signalServiceConfiguration(),
|
||||
"signal-cli-test",
|
||||
null,
|
||||
false,
|
||||
1000L,
|
||||
new SecureRandom(),
|
||||
client);
|
||||
final var api = new RegistrationApiV2(restClient, false);
|
||||
|
||||
try (final var account = SignalAccount.createLinkedAccount(directory.toFile(),
|
||||
"account",
|
||||
ServiceEnvironment.STAGING,
|
||||
Settings.DEFAULT)) {
|
||||
account.setProvisioningData(number,
|
||||
ACI_ID,
|
||||
pni,
|
||||
"test-password",
|
||||
new byte[]{1},
|
||||
KeyUtils.generateIdentityKeyPair(),
|
||||
pni == null ? null : KeyUtils.generateIdentityKeyPair(),
|
||||
KeyUtils.createProfileKey(),
|
||||
null,
|
||||
new byte[32],
|
||||
null);
|
||||
final var aciKeys = KeyUtils.generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||
final var pniKeys = pni == null
|
||||
? null
|
||||
: KeyUtils.generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||
if (statusCode == 200) {
|
||||
assertEquals(2,
|
||||
ProvisioningManagerImpl.registerLinkedDevice(api, account, "test-code", aciKeys, pniKeys));
|
||||
} else {
|
||||
final var error = assertThrows(IOException.class,
|
||||
() -> ProvisioningManagerImpl.registerLinkedDevice(api,
|
||||
account,
|
||||
"test-code",
|
||||
aciKeys,
|
||||
pniKeys));
|
||||
assertEquals(errorMessage, error.getMessage());
|
||||
if (statusCode == 403) {
|
||||
assertInstanceOf(AuthorizationFailedException.class, error);
|
||||
}
|
||||
}
|
||||
assertEquals("PUT", request.get().method());
|
||||
assertEquals("/v1/devices/link", request.get().url().encodedPath());
|
||||
assertEquals(Credentials.basic(ACI_ID.toString(), "test-password"), request.get().header("Authorization"));
|
||||
assertTrue(body.get()
|
||||
.path("accountAttributes")
|
||||
.path("capabilities")
|
||||
.path("optionalPhoneNumber")
|
||||
.asBoolean());
|
||||
assertTrue(body.get().hasNonNull("aciSignedPreKey"));
|
||||
assertTrue(body.get().hasNonNull("aciPqLastResortPreKey"));
|
||||
assertEquals(pni != null, body.get().has("pniSignedPreKey"));
|
||||
assertEquals(pni != null, body.get().has("pniPqLastResortPreKey"));
|
||||
assertEquals(pni != null, body.get().path("accountAttributes").has("pniRegistrationId"));
|
||||
} finally {
|
||||
client.dispatcher().executorService().shutdown();
|
||||
client.connectionPool().evictAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,115 +0,0 @@
|
||||
package org.asamk.signal.manager.storage;
|
||||
|
||||
import org.asamk.signal.manager.Settings;
|
||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.signal.core.models.AccountEntropyPool;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class NumberlessAccountTest {
|
||||
|
||||
@TempDir
|
||||
Path directory;
|
||||
|
||||
@Test
|
||||
void numberlessLinkedAccountSurvivesReload() throws Exception {
|
||||
final var aci = ACI.parseOrThrow("11111111-1111-4111-8111-111111111111");
|
||||
final var identity = KeyUtils.generateIdentityKeyPair();
|
||||
final var salt = new byte[32];
|
||||
salt[0] = 42;
|
||||
try (final var account = SignalAccount.createLinkedAccount(directory.toFile(),
|
||||
"account",
|
||||
ServiceEnvironment.STAGING,
|
||||
Settings.DEFAULT)) {
|
||||
account.setProvisioningData(null,
|
||||
aci,
|
||||
null,
|
||||
"test-password",
|
||||
new byte[]{1},
|
||||
identity,
|
||||
null,
|
||||
KeyUtils.createProfileKey(),
|
||||
null,
|
||||
salt,
|
||||
null);
|
||||
account.finishLinking(2, KeyUtils.generatePreKeysForType(account.getAccountData(ServiceIdType.ACI)), null);
|
||||
}
|
||||
|
||||
try (final var account = SignalAccount.load(directory.toFile(), "account", true, Settings.DEFAULT)) {
|
||||
assertTrue(account.isRegistered());
|
||||
assertFalse(account.isPrimaryDevice());
|
||||
assertEquals(2, account.getDeviceId());
|
||||
assertEquals(aci, account.getAci());
|
||||
assertNull(account.getNumber());
|
||||
assertNull(account.getPni());
|
||||
assertNull(account.getPniIdentityKeyPair());
|
||||
assertNull(account.getSignalServiceDataStore().pniOrNull());
|
||||
assertArrayEquals(identity.serialize(), account.getAciIdentityKeyPair().serialize());
|
||||
assertArrayEquals(salt, account.getAuthCredentialSalt());
|
||||
assertNull(account.getAccountAttributesV2().getPniRegistrationId());
|
||||
assertNull(account.getAccountAttributesV2().getDiscoverableByPhoneNumber());
|
||||
assertTrue(account.getAccountAttributesV2().getCapabilities().getOptionalPhoneNumber());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void recoveredNumberlessPrimaryPreservesRecoveryMaterialAndDropsPniIdentity() throws Exception {
|
||||
final var aci = ACI.parseOrThrow("11111111-1111-4111-8111-111111111111");
|
||||
final var entropyPool = AccountEntropyPool.Companion.generate();
|
||||
final var salt = new byte[32];
|
||||
salt[0] = 42;
|
||||
try (final var account = SignalAccount.create(directory.toFile(),
|
||||
"account",
|
||||
null,
|
||||
aci,
|
||||
ServiceEnvironment.STAGING,
|
||||
KeyUtils.generateIdentityKeyPair(),
|
||||
KeyUtils.generateIdentityKeyPair(),
|
||||
KeyUtils.createProfileKey(),
|
||||
Settings.DEFAULT)) {
|
||||
final var aciPreKeys = KeyUtils.generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||
final var pniPreKeys = KeyUtils.generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||
account.finishRecoveryRegistration(aci, null, null, entropyPool, salt, aciPreKeys, pniPreKeys);
|
||||
}
|
||||
|
||||
try (final var account = SignalAccount.load(directory.toFile(), "account", true, Settings.DEFAULT)) {
|
||||
assertTrue(account.isRegistered());
|
||||
assertTrue(account.isPrimaryDevice());
|
||||
assertEquals(aci, account.getAci());
|
||||
assertNull(account.getNumber());
|
||||
assertNull(account.getPni());
|
||||
assertNull(account.getPniIdentityKeyPair());
|
||||
assertEquals(entropyPool.getValue(), account.getAccountEntropyPool().getValue());
|
||||
assertArrayEquals(salt, account.getAuthCredentialSalt());
|
||||
assertTrue(account.getAccountAttributesV2().getCapabilities().getOptionalPhoneNumber());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void recoveryAttributesOmitPhoneNumberDiscoverabilityForNumberedAccount() throws Exception {
|
||||
final var aci = ACI.parseOrThrow("11111111-1111-4111-8111-111111111111");
|
||||
try (final var account = SignalAccount.create(directory.toFile(),
|
||||
"account",
|
||||
"+12025550123",
|
||||
aci,
|
||||
ServiceEnvironment.STAGING,
|
||||
KeyUtils.generateIdentityKeyPair(),
|
||||
KeyUtils.generateIdentityKeyPair(),
|
||||
KeyUtils.createProfileKey(),
|
||||
Settings.DEFAULT)) {
|
||||
assertNull(account.getAccountAttributesV2ForRecovery(null, "recovery-password")
|
||||
.getDiscoverableByPhoneNumber());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
package org.asamk.signal.manager.storage.accounts;
|
||||
|
||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AccountsStoreTest {
|
||||
|
||||
private static final ACI OLD_ACI = ACI.parseOrThrow("11111111-1111-4111-8111-111111111111");
|
||||
private static final ACI NEW_ACI = ACI.parseOrThrow("22222222-2222-4222-8222-222222222222");
|
||||
|
||||
@TempDir
|
||||
Path directory;
|
||||
|
||||
@Test
|
||||
void discoversNumberlessAccountsByAciAndKeepsEnvironmentsSeparate() throws Exception {
|
||||
final var store = new AccountsStore(directory.toFile(), ServiceEnvironment.STAGING, path -> null);
|
||||
final var aci = ACI.parseOrThrow("11111111-1111-4111-8111-111111111111");
|
||||
final var path = store.addAccount(null, aci);
|
||||
store.addAccount("+12025550123", null);
|
||||
|
||||
final var reopened = new AccountsStore(directory.toFile(), ServiceEnvironment.STAGING, ignored -> null);
|
||||
assertEquals(2, reopened.getAllAccounts().size());
|
||||
assertEquals(path, reopened.getPathByAci(aci));
|
||||
assertNull(reopened.getPathByNumber(null));
|
||||
assertEquals(Set.of("+12025550123"), reopened.getAllNumbers());
|
||||
assertTrue(new AccountsStore(directory.toFile(), ServiceEnvironment.LIVE, ignored -> null).getAllAccounts()
|
||||
.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatingAnAciDoesNotLeaveDuplicateNumberlessAccounts() throws Exception {
|
||||
final var store = new AccountsStore(directory.toFile(), ServiceEnvironment.STAGING, path -> null);
|
||||
final var aci = ACI.parseOrThrow("11111111-1111-4111-8111-111111111111");
|
||||
final var oldPath = store.addAccount(null, aci);
|
||||
Files.createFile(directory.resolve(oldPath));
|
||||
final var newPath = store.addAccount(null, null);
|
||||
store.updateAccount(newPath, null, aci);
|
||||
|
||||
assertEquals(newPath, store.getPathByAci(aci));
|
||||
assertEquals(1, store.getAllAccounts().size());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void replacingANumberKeepsTheOldAccountAvailableByAci(final boolean update) throws Exception {
|
||||
final var store = new AccountsStore(directory.toFile(), ServiceEnvironment.STAGING, path -> null);
|
||||
final var oldPath = store.addAccount("+12025550123", OLD_ACI);
|
||||
Files.createFile(directory.resolve(oldPath));
|
||||
final String newPath;
|
||||
if (update) {
|
||||
newPath = store.addAccount("+12025550124", NEW_ACI);
|
||||
store.updateAccount(newPath, "+12025550123", NEW_ACI);
|
||||
} else {
|
||||
newPath = store.addAccount("+12025550123", NEW_ACI);
|
||||
}
|
||||
|
||||
final var reopened = new AccountsStore(directory.toFile(), ServiceEnvironment.STAGING, path -> null);
|
||||
assertEquals(Set.of(oldPath, newPath), getAccountPaths(reopened));
|
||||
assertEquals(newPath, reopened.getPathByNumber("+12025550123"));
|
||||
assertEquals(oldPath, reopened.getPathByAci(OLD_ACI));
|
||||
}
|
||||
|
||||
private Set<String> getAccountPaths(final AccountsStore store) throws IOException {
|
||||
return store.getAllAccounts().stream().map(AccountsStorage.Account::path).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
}
|
||||
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