mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-09-19 05:58:57 +00:00
Compare commits
No commits in common. "master" and "v0.14.4" have entirely different histories.
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
|
|
||||||
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
@ -35,19 +35,45 @@ jobs:
|
|||||||
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
||||||
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create draft release and upload assets
|
- name: Create release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
with:
|
||||||
gh release create "v${{ steps.version.outputs.version }}" \
|
tag_name: v${{ steps.version.outputs.version }} # note: added `v`
|
||||||
--repo "${{ github.repository }}" \
|
release_name: v${{ steps.version.outputs.version }} # note: added `v`
|
||||||
--title "v${{ steps.version.outputs.version }}" \
|
draft: true
|
||||||
--draft \
|
|
||||||
--verify-tag \
|
- name: Upload archive
|
||||||
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
|
uses: actions/upload-release-asset@v1
|
||||||
"signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \
|
env:
|
||||||
"signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
"signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz"
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: signal-cli-${{ steps.version.outputs.version }}.tar.gz
|
||||||
|
asset_name: signal-cli-${{ steps.version.outputs.version }}.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.version.outputs.version }}-Linux-native.tar.gz
|
||||||
|
asset_name: signal-cli-${{ steps.version.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.version.outputs.version }}-Linux-client.tar.gz
|
||||||
|
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
|
||||||
|
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||||
|
|
||||||
build-container:
|
build-container:
|
||||||
needs: release
|
needs: release
|
||||||
@ -68,7 +94,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v3
|
uses: redhat-actions/buildah-build@v2
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
||||||
@ -76,7 +102,7 @@ jobs:
|
|||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v3
|
uses: redhat-actions/push-to-registry@v2
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
@ -109,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v3
|
uses: redhat-actions/buildah-build@v2
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
||||||
@ -117,7 +143,7 @@ jobs:
|
|||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v3
|
uses: redhat-actions/push-to-registry@v2
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
@ -150,7 +176,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v3
|
uses: redhat-actions/buildah-build@v2
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
||||||
@ -158,7 +184,7 @@ jobs:
|
|||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v3
|
uses: redhat-actions/push-to-registry@v2
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,4 +20,3 @@ man/*.1
|
|||||||
man/*.5
|
man/*.5
|
||||||
man/man1
|
man/man1
|
||||||
man/man5
|
man/man5
|
||||||
.superpowers/
|
|
||||||
|
|||||||
65
CHANGELOG.md
65
CHANGELOG.md
@ -1,70 +1,5 @@
|
|||||||
# Changelog
|
# 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
|
## [0.14.4] - 2026-05-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
14
README.md
14
README.md
@ -3,7 +3,7 @@
|
|||||||
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
||||||
It supports registering, verifying, sending and receiving messages.
|
It supports registering, verifying, sending and receiving messages.
|
||||||
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
||||||
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/lib/libsignal-service).
|
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal-service).
|
||||||
For registering you need a phone number where you can receive SMS or incoming calls.
|
For registering you need a phone number where you can receive SMS or incoming calls.
|
||||||
|
|
||||||
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
||||||
@ -64,20 +64,8 @@ Important: The ACCOUNT is your phone number in international format and must inc
|
|||||||
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
|
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
|
||||||
of all country codes.)
|
of all country codes.)
|
||||||
|
|
||||||
* Link to an existing account
|
|
||||||
|
|
||||||
If you have an existing Signal account associated
|
|
||||||
with a number, you can link signal-cli to it with:
|
|
||||||
|
|
||||||
signal-cli link
|
|
||||||
|
|
||||||
* Register a number (with SMS verification)
|
* Register a number (with SMS verification)
|
||||||
|
|
||||||
Alternatively, if you don't have an existing Signal
|
|
||||||
account, you can register one from signal-cli. Note
|
|
||||||
that this will unregister any existing client
|
|
||||||
associated with the same number.
|
|
||||||
|
|
||||||
signal-cli -a ACCOUNT register
|
signal-cli -a ACCOUNT register
|
||||||
|
|
||||||
You can register Signal using a landline number. In this case, you need to follow the procedure below:
|
You can register Signal using a landline number. In this case, you need to follow the procedure below:
|
||||||
|
|||||||
@ -5,12 +5,12 @@ plugins {
|
|||||||
application
|
application
|
||||||
eclipse
|
eclipse
|
||||||
`check-lib-versions`
|
`check-lib-versions`
|
||||||
id("org.graalvm.buildtools.native") version "1.1.12"
|
id("org.graalvm.buildtools.native") version "1.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "org.asamk"
|
group = "org.asamk"
|
||||||
version = "0.14.9-SNAPSHOT"
|
version = "0.14.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@ -74,7 +74,7 @@ val excludePatterns = mapOf(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val schemaAnnotationProcessor = configurations.create("schemaAnnotationProcessor") {
|
val schemaAnnotationProcessor by configurations.creating {
|
||||||
isCanBeConsumed = false
|
isCanBeConsumed = false
|
||||||
isCanBeResolved = true
|
isCanBeResolved = true
|
||||||
}
|
}
|
||||||
|
|||||||
363
client/Cargo.lock
generated
363
client/Cargo.lock
generated
@ -54,19 +54,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.104"
|
version = "1.0.102"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.92"
|
version = "0.1.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -83,21 +83,21 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.2"
|
version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.12.1"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.4.5"
|
version = "1.2.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
|
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@ -117,9 +117,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.6"
|
version = "4.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@ -127,9 +127,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.6.6"
|
version = "4.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@ -140,14 +140,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.6.4"
|
version = "4.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -164,9 +164,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.8"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
|
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -190,13 +190,13 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.7"
|
version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -217,9 +217,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.12"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
@ -238,53 +238,53 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-timer"
|
name = "futures-timer"
|
||||||
version = "3.0.4"
|
version = "3.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
|
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@ -307,9 +307,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.19"
|
version = "0.4.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
|
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -326,9 +326,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@ -338,9 +338,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.5.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"itoa",
|
"itoa",
|
||||||
@ -348,9 +348,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body"
|
name = "http-body"
|
||||||
version = "1.1.0"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
@ -358,9 +358,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body-util"
|
name = "http-body-util"
|
||||||
version = "0.1.5"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -377,9 +377,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.11.1"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
|
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -434,9 +434,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
|
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"potential_utf",
|
"potential_utf",
|
||||||
@ -448,9 +448,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_locale_core"
|
name = "icu_locale_core"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"litemap",
|
"litemap",
|
||||||
@ -461,9 +461,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer"
|
name = "icu_normalizer"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
|
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_normalizer_data",
|
"icu_normalizer_data",
|
||||||
@ -475,17 +475,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer_data"
|
name = "icu_normalizer_data"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
|
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
|
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
"icu_properties_data",
|
"icu_properties_data",
|
||||||
@ -496,15 +495,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
|
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
version = "2.3.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
@ -528,9 +527,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna_adapter"
|
name = "idna_adapter"
|
||||||
version = "1.2.2"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_normalizer",
|
"icu_normalizer",
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
@ -538,9 +537,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.2"
|
version = "2.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
@ -599,7 +598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -633,7 +632,7 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tower",
|
"tower",
|
||||||
@ -657,7 +656,7 @@ dependencies = [
|
|||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"url",
|
"url",
|
||||||
@ -673,7 +672,7 @@ dependencies = [
|
|||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -685,14 +684,14 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.189"
|
version = "0.2.185"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
@ -702,27 +701,27 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.3"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.34"
|
version = "0.4.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.3"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
@ -755,22 +754,22 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.13"
|
version = "1.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
|
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-internal",
|
"pin-project-internal",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-internal"
|
name = "pin-project-internal"
|
||||||
version = "1.1.13"
|
version = "1.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
|
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -781,9 +780,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.6"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
|
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
@ -799,18 +798,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.47"
|
version = "1.0.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@ -831,9 +830,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.3"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
@ -850,9 +849,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.44"
|
version = "0.23.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"
|
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -865,9 +864,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-native-certs"
|
name = "rustls-native-certs"
|
||||||
version = "0.8.4"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"openssl-probe",
|
"openssl-probe",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@ -877,9 +876,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.15.1"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -913,9 +912,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.15"
|
version = "0.103.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@ -965,9 +964,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@ -975,29 +974,29 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.229"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.229"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.151"
|
version = "1.0.149"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -1008,13 +1007,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-cli-client"
|
name = "signal-cli-client"
|
||||||
version = "0.14.8"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -1023,7 +1022,7 @@ dependencies = [
|
|||||||
"jsonrpsee",
|
"jsonrpsee",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
@ -1036,15 +1035,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.16.0"
|
version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.5"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@ -1070,20 +1069,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.119"
|
version = "2.0.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "3.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1104,7 +1092,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1128,11 +1116,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.20"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.20",
|
"thiserror-impl 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1143,25 +1131,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.20"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.4"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
|
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
@ -1169,9 +1157,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.1"
|
version = "1.52.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@ -1184,20 +1172,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.7.2"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.5"
|
version = "0.26.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
|
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1205,9 +1193,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.19"
|
version = "0.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
|
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@ -1216,14 +1204,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.19"
|
version = "0.7.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"libc",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@ -1239,9 +1226,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.13+spec-1.1.0"
|
version = "0.25.11+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
@ -1251,9 +1238,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.3+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
@ -1303,7 +1290,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1388,14 +1375,14 @@ version = "0.26.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
|
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webpki-root-certs 1.0.9",
|
"webpki-root-certs 1.0.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.9"
|
version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@ -1574,24 +1561,24 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.4"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.4"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.3"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"yoke-derive",
|
"yoke-derive",
|
||||||
@ -1606,15 +1593,15 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerofrom-derive",
|
"zerofrom-derive",
|
||||||
]
|
]
|
||||||
@ -1627,21 +1614,21 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.9.0"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.5"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
|
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"yoke",
|
"yoke",
|
||||||
@ -1650,9 +1637,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec"
|
name = "zerovec"
|
||||||
version = "0.11.8"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
@ -1661,17 +1648,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec-derive"
|
name = "zerovec-derive"
|
||||||
version = "0.11.6"
|
version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.23"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "signal-cli-client"
|
name = "signal-cli-client"
|
||||||
version = "0.14.8"
|
version = "0.0.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
@ -45,18 +45,6 @@
|
|||||||
<content_attribute id="social-chat">intense</content_attribute>
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
</content_rating>
|
</content_rating>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="0.14.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">
|
<release version="0.14.4" date="2026-05-23">
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
[versions]
|
[versions]
|
||||||
slf4j = "2.0.19"
|
slf4j = "2.0.18"
|
||||||
coroutines = "1.11.0"
|
junit = "6.1.0"
|
||||||
junit = "6.1.3"
|
micronaut-json-schema = "2.0.1"
|
||||||
micronaut-json-schema = "2.2.0"
|
micronaut-core = "5.0.0"
|
||||||
micronaut-core = "5.2.0"
|
signal-service = "2.15.3_unofficial_147"
|
||||||
signal-service = "2.15.3_unofficial_153"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85.2"
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
||||||
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.2"
|
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.20.2"
|
||||||
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
|
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
|
||||||
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
|
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
|
||||||
zxing = "com.google.zxing:core:3.5.4"
|
zxing = "com.google.zxing:core:3.5.4"
|
||||||
@ -18,12 +17,11 @@ micronaut-json-schema-generator = { module = "io.micronaut.jsonschema:micronaut-
|
|||||||
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-core" }
|
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-core" }
|
||||||
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||||
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
||||||
logback = "ch.qos.logback:logback-classic:1.6.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" }
|
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
|
||||||
sqlite = "org.xerial:sqlite-jdbc:3.53.4.0"
|
sqlite = "org.xerial:sqlite-jdbc:3.53.1.0"
|
||||||
hikari = "com.zaxxer:HikariCP:7.1.0"
|
hikari = "com.zaxxer:HikariCP:7.0.2"
|
||||||
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
|
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
|
||||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||||
junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }
|
junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
retries=0
|
retries=0
|
||||||
retryBackOffMs=500
|
retryBackOffMs=500
|
||||||
|
|||||||
6
gradlew
vendored
6
gradlew
vendored
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# gradlew start up script for POSIX generated by Gradle.
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
#
|
#
|
||||||
# Important for running:
|
# Important for running:
|
||||||
#
|
#
|
||||||
@ -29,7 +29,7 @@
|
|||||||
# bash, then to run this script, type that shell name before the whole
|
# bash, then to run this script, type that shell name before the whole
|
||||||
# command line, like:
|
# command line, like:
|
||||||
#
|
#
|
||||||
# ksh gradlew
|
# ksh Gradle
|
||||||
#
|
#
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
4
gradlew.bat
vendored
4
gradlew.bat
vendored
@ -19,7 +19,7 @@
|
|||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem gradlew startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute gradlew
|
@rem Execute Gradle
|
||||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
@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
|
@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
|
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
|
||||||
|
|||||||
@ -30,7 +30,6 @@ dependencies {
|
|||||||
implementation(libs.slf4j.api)
|
implementation(libs.slf4j.api)
|
||||||
implementation(libs.sqlite)
|
implementation(libs.sqlite)
|
||||||
implementation(libs.hikari)
|
implementation(libs.hikari)
|
||||||
compileOnly(libs.kotlinx.coroutines.core)
|
|
||||||
|
|
||||||
testImplementation(libs.junit.jupiter)
|
testImplementation(libs.junit.jupiter)
|
||||||
testImplementation(platform(libs.junit.jupiter.bom))
|
testImplementation(platform(libs.junit.jupiter.bom))
|
||||||
|
|||||||
@ -54,7 +54,6 @@ import org.asamk.signal.manager.api.UserStatus;
|
|||||||
import org.asamk.signal.manager.api.UsernameLinkUrl;
|
import org.asamk.signal.manager.api.UsernameLinkUrl;
|
||||||
import org.asamk.signal.manager.api.UsernameStatus;
|
import org.asamk.signal.manager.api.UsernameStatus;
|
||||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||||
import org.signal.core.util.UuidUtil;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -75,10 +74,6 @@ public interface Manager extends Closeable {
|
|||||||
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isValidAci(final String aci) {
|
|
||||||
return UuidUtil.INSTANCE.isUuid(aci);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isSignalClientAvailable() {
|
static boolean isSignalClientAvailable() {
|
||||||
final Logger logger = LoggerFactory.getLogger(Manager.class);
|
final Logger logger = LoggerFactory.getLogger(Manager.class);
|
||||||
try {
|
try {
|
||||||
@ -96,8 +91,6 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
String getSelfNumber();
|
String getSelfNumber();
|
||||||
|
|
||||||
String getSelfACI();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is used for checking a set of phone numbers for registration on Signal
|
* This is used for checking a set of phone numbers for registration on Signal
|
||||||
*
|
*
|
||||||
@ -184,10 +177,6 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
void deleteGroup(GroupId groupId) throws IOException;
|
void deleteGroup(GroupId groupId) throws IOException;
|
||||||
|
|
||||||
SendGroupMessageResults terminateGroup(
|
|
||||||
GroupId groupId
|
|
||||||
) throws IOException, GroupNotFoundException, NotAGroupMemberException;
|
|
||||||
|
|
||||||
Pair<GroupId, SendGroupMessageResults> createGroup(
|
Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
String name,
|
String name,
|
||||||
Set<RecipientIdentifier.Single> members,
|
Set<RecipientIdentifier.Single> members,
|
||||||
@ -224,19 +213,6 @@ public interface Manager extends Closeable {
|
|||||||
long editTargetTimestamp
|
long editTargetTimestamp
|
||||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||||
|
|
||||||
/**
|
|
||||||
* Post a file attachment story to "My Story" or to a group.
|
|
||||||
*
|
|
||||||
* @param attachment path to the file to upload and post as a story
|
|
||||||
* @param allowsReplies whether other users are allowed to reply to this story
|
|
||||||
* @param groupId if present, post the story to this group instead of "My Story"
|
|
||||||
*/
|
|
||||||
SendMessageResults sendStory(
|
|
||||||
String attachment,
|
|
||||||
boolean allowsReplies,
|
|
||||||
Optional<GroupId> groupId
|
|
||||||
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException;
|
|
||||||
|
|
||||||
SendMessageResults sendRemoteDeleteMessage(
|
SendMessageResults sendRemoteDeleteMessage(
|
||||||
long targetSentTimestamp,
|
long targetSentTimestamp,
|
||||||
Set<RecipientIdentifier> recipients
|
Set<RecipientIdentifier> recipients
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import org.asamk.signal.manager.internal.RegistrationManagerImpl;
|
|||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
@ -68,7 +67,7 @@ public class SignalAccountFiles {
|
|||||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||||
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||||
try {
|
try {
|
||||||
return new Pair<Manager, Throwable>(initManagerByNumber(a.number(), a.path()), null);
|
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
|
||||||
} catch (NotRegisteredException e) {
|
} catch (NotRegisteredException e) {
|
||||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||||
return null;
|
return null;
|
||||||
@ -91,31 +90,15 @@ public class SignalAccountFiles {
|
|||||||
return new MultiAccountManagerImpl(managers, this);
|
return new MultiAccountManagerImpl(managers, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
final var accountPath = accountsStore.getPathByNumber(number);
|
final var accountPath = accountsStore.getPathByNumber(number);
|
||||||
return this.initManagerByNumber(number, accountPath);
|
return this.initManager(number, accountPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Manager initManagerByAci(String aciStr) throws IOException, NotRegisteredException, AccountCheckException {
|
private Manager initManager(
|
||||||
final var aci = ACI.parseOrThrow(aciStr);
|
|
||||||
final var accountPath = accountsStore.getPathByAci(aci);
|
|
||||||
return this.initManagerByAci(aci, accountPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manager initManagerByNumber(
|
|
||||||
String number,
|
String number,
|
||||||
String accountPath
|
String accountPath
|
||||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
final var account = loadAccount(accountPath);
|
|
||||||
if (!number.equals(account.getNumber())) {
|
|
||||||
account.close();
|
|
||||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
|
||||||
}
|
|
||||||
|
|
||||||
return initManagerFromAccount(number, accountPath, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SignalAccount loadAccount(final String accountPath) throws NotRegisteredException, IOException {
|
|
||||||
if (accountPath == null) {
|
if (accountPath == null) {
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
}
|
}
|
||||||
@ -123,27 +106,12 @@ public class SignalAccountFiles {
|
|||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||||
}
|
if (!number.equals(account.getNumber())) {
|
||||||
|
|
||||||
private Manager initManagerByAci(
|
|
||||||
ACI aci,
|
|
||||||
String accountPath
|
|
||||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
|
||||||
final var account = loadAccount(accountPath);
|
|
||||||
if (!aci.equals(account.getAci())) {
|
|
||||||
account.close();
|
account.close();
|
||||||
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci());
|
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
return initManagerFromAccount(aci.toString(), accountPath, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ManagerImpl initManagerFromAccount(
|
|
||||||
final String identifier,
|
|
||||||
final String accountPath,
|
|
||||||
final SignalAccount account
|
|
||||||
) throws NotRegisteredException, IOException, AccountCheckException {
|
|
||||||
if (!account.isRegistered()) {
|
if (!account.isRegistered()) {
|
||||||
account.close();
|
account.close();
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
@ -168,7 +136,7 @@ public class SignalAccountFiles {
|
|||||||
throw new IOException("signal-cli version is too old for the Signal-Server, please update.");
|
throw new IOException("signal-cli version is too old for the Signal-Server, please update.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
manager.close();
|
manager.close();
|
||||||
throw new AccountCheckException("Error while checking account " + identifier + ": " + e.getMessage(), e);
|
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.getServiceEnvironment() == null) {
|
if (account.getServiceEnvironment() == null) {
|
||||||
|
|||||||
@ -1,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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,7 +15,6 @@ public record Contact(
|
|||||||
long muteUntil,
|
long muteUntil,
|
||||||
boolean hideStory,
|
boolean hideStory,
|
||||||
boolean isBlocked,
|
boolean isBlocked,
|
||||||
long blockedAt,
|
|
||||||
boolean isArchived,
|
boolean isArchived,
|
||||||
boolean isProfileSharingEnabled,
|
boolean isProfileSharingEnabled,
|
||||||
boolean isHidden,
|
boolean isHidden,
|
||||||
@ -35,7 +34,6 @@ public record Contact(
|
|||||||
builder.muteUntil,
|
builder.muteUntil,
|
||||||
builder.hideStory,
|
builder.hideStory,
|
||||||
builder.isBlocked,
|
builder.isBlocked,
|
||||||
builder.blockedAt,
|
|
||||||
builder.isArchived,
|
builder.isArchived,
|
||||||
builder.isProfileSharingEnabled,
|
builder.isProfileSharingEnabled,
|
||||||
builder.isHidden,
|
builder.isHidden,
|
||||||
@ -60,7 +58,6 @@ public record Contact(
|
|||||||
builder.muteUntil = copy.muteUntil();
|
builder.muteUntil = copy.muteUntil();
|
||||||
builder.hideStory = copy.hideStory();
|
builder.hideStory = copy.hideStory();
|
||||||
builder.isBlocked = copy.isBlocked();
|
builder.isBlocked = copy.isBlocked();
|
||||||
builder.blockedAt = copy.blockedAt();
|
|
||||||
builder.isArchived = copy.isArchived();
|
builder.isArchived = copy.isArchived();
|
||||||
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
||||||
builder.isHidden = copy.isHidden();
|
builder.isHidden = copy.isHidden();
|
||||||
@ -83,21 +80,6 @@ public record Contact(
|
|||||||
return givenName + " " + familyName;
|
return givenName + " " + familyName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDisplayNickname() {
|
|
||||||
final var noNickGivenName = Util.isEmpty(nickNameGivenName);
|
|
||||||
final var noNickFamilyName = Util.isEmpty(nickNameFamilyName);
|
|
||||||
|
|
||||||
if (noNickGivenName && noNickFamilyName) {
|
|
||||||
return null;
|
|
||||||
} else if (noNickGivenName) {
|
|
||||||
return nickNameFamilyName;
|
|
||||||
} else if (noNickFamilyName) {
|
|
||||||
return nickNameGivenName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nickNameGivenName + " " + nickNameFamilyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
|
|
||||||
private String givenName;
|
private String givenName;
|
||||||
@ -112,7 +94,6 @@ public record Contact(
|
|||||||
private long muteUntil;
|
private long muteUntil;
|
||||||
private boolean hideStory;
|
private boolean hideStory;
|
||||||
private boolean isBlocked;
|
private boolean isBlocked;
|
||||||
private long blockedAt;
|
|
||||||
private boolean isArchived;
|
private boolean isArchived;
|
||||||
private boolean isProfileSharingEnabled;
|
private boolean isProfileSharingEnabled;
|
||||||
private boolean isHidden;
|
private boolean isHidden;
|
||||||
@ -181,20 +162,10 @@ public record Contact(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Builder withIsBlocked(final boolean val) {
|
public Builder withIsBlocked(final boolean val) {
|
||||||
if (val && !isBlocked) {
|
|
||||||
blockedAt = System.currentTimeMillis();
|
|
||||||
} else if (!val) {
|
|
||||||
blockedAt = 0;
|
|
||||||
}
|
|
||||||
isBlocked = val;
|
isBlocked = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withBlockedAt(final long val) {
|
|
||||||
blockedAt = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withIsArchived(final boolean val) {
|
public Builder withIsArchived(final boolean val) {
|
||||||
isArchived = val;
|
isArchived = val;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@ -22,8 +22,7 @@ public record Group(
|
|||||||
GroupPermission permissionEditDetails,
|
GroupPermission permissionEditDetails,
|
||||||
GroupPermission permissionSendMessage,
|
GroupPermission permissionSendMessage,
|
||||||
boolean isMember,
|
boolean isMember,
|
||||||
boolean isAdmin,
|
boolean isAdmin
|
||||||
boolean isTerminated
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static Group from(
|
public static Group from(
|
||||||
@ -60,7 +59,6 @@ public record Group(
|
|||||||
groupInfo.getPermissionEditDetails(),
|
groupInfo.getPermissionEditDetails(),
|
||||||
groupInfo.getPermissionSendMessage(),
|
groupInfo.getPermissionSendMessage(),
|
||||||
groupInfo.isMember(selfRecipientId),
|
groupInfo.isMember(selfRecipientId),
|
||||||
groupInfo.isAdmin(selfRecipientId),
|
groupInfo.isAdmin(selfRecipientId));
|
||||||
groupInfo.isTerminated());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import java.util.Optional;
|
|||||||
public record Message(
|
public record Message(
|
||||||
String messageText,
|
String messageText,
|
||||||
List<String> attachments,
|
List<String> attachments,
|
||||||
List<AttachmentDimensions> attachmentDimensions,
|
|
||||||
List<String> attachmentBlurHashes,
|
|
||||||
boolean viewOnce,
|
boolean viewOnce,
|
||||||
boolean voiceNote,
|
boolean voiceNote,
|
||||||
List<Mention> mentions,
|
List<Mention> mentions,
|
||||||
@ -19,8 +17,6 @@ public record Message(
|
|||||||
boolean urgent
|
boolean urgent
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public record AttachmentDimensions(int width, int height) {}
|
|
||||||
|
|
||||||
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
||||||
|
|
||||||
public record Quote(
|
public record Quote(
|
||||||
|
|||||||
@ -739,10 +739,7 @@ public record MessageEnvelope(
|
|||||||
null,
|
null,
|
||||||
d.getE164(),
|
d.getE164(),
|
||||||
null))
|
null))
|
||||||
.toList(),
|
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
|
||||||
blockedListMessage.groups.stream()
|
|
||||||
.map(group -> GroupId.unknownVersion(group.getGroupId()))
|
|
||||||
.toList());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,15 @@ package org.asamk.signal.manager.config;
|
|||||||
import org.signal.libsignal.net.Network.Environment;
|
import org.signal.libsignal.net.Network.Environment;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.signal.network.config.HttpProxy;
|
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||||
import org.signal.network.config.SignalCdnUrl;
|
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||||
import org.signal.network.config.SignalCdsiUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||||
import org.signal.network.config.SignalProxy;
|
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||||
import org.signal.network.config.SignalServiceConfiguration;
|
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||||
import org.signal.network.config.SignalServiceUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||||
import org.signal.network.config.SignalStorageUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
||||||
import org.signal.network.config.SignalSvr2Url;
|
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
||||||
import org.signal.network.config.TrustStore;
|
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -30,8 +30,7 @@ class LiveConfig {
|
|||||||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
||||||
.decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6");
|
.decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6");
|
||||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||||
private static final String SVR2_MRENCLAVE = "fdbbacdc0c043d0d53fe1440f62728de0386f45ab0a275bd8f99e03a02af355e";
|
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
|
||||||
private static final String SVR2_MRENCLAVE_LEGACY = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4";
|
|
||||||
|
|
||||||
private static final String URL = "https://chat.signal.org";
|
private static final String URL = "https://chat.signal.org";
|
||||||
private static final String CDN_URL = "https://cdn.signal.org";
|
private static final String CDN_URL = "https://cdn.signal.org";
|
||||||
@ -49,10 +48,10 @@ class LiveConfig {
|
|||||||
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
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==");
|
.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()
|
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()
|
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;
|
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
|
||||||
|
|
||||||
@ -94,7 +93,7 @@ class LiveConfig {
|
|||||||
createDefaultServiceConfiguration(interceptors),
|
createDefaultServiceConfiguration(interceptors),
|
||||||
getUnidentifiedSenderTrustRoots(),
|
getUnidentifiedSenderTrustRoots(),
|
||||||
CDSI_MRENCLAVE,
|
CDSI_MRENCLAVE,
|
||||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
List.of(SVR2_MRENCLAVE));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LiveConfig() {
|
private LiveConfig() {
|
||||||
|
|||||||
@ -31,14 +31,7 @@ public class ServiceConfig {
|
|||||||
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
||||||
final var attachmentBackfill = !isPrimaryDevice;
|
final var attachmentBackfill = !isPrimaryDevice;
|
||||||
final var spqr = true;
|
final var spqr = true;
|
||||||
final var usernameSyncChangeMessage = !isPrimaryDevice;
|
return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr);
|
||||||
final var optionalPhoneNumber = !isPrimaryDevice;
|
|
||||||
return new AccountAttributes.Capabilities(true,
|
|
||||||
true,
|
|
||||||
attachmentBackfill,
|
|
||||||
spqr,
|
|
||||||
usernameSyncChangeMessage,
|
|
||||||
optionalPhoneNumber);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package org.asamk.signal.manager.config;
|
|||||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.signal.network.config.SignalServiceConfiguration;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,15 @@ package org.asamk.signal.manager.config;
|
|||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.signal.network.config.HttpProxy;
|
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||||
import org.signal.network.config.SignalCdnUrl;
|
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||||
import org.signal.network.config.SignalCdsiUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||||
import org.signal.network.config.SignalProxy;
|
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||||
import org.signal.network.config.SignalServiceConfiguration;
|
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||||
import org.signal.network.config.SignalServiceUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||||
import org.signal.network.config.SignalStorageUrl;
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
||||||
import org.signal.network.config.SignalSvr2Url;
|
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
||||||
import org.signal.network.config.TrustStore;
|
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -30,8 +30,7 @@ class StagingConfig {
|
|||||||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
||||||
.decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm");
|
.decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm");
|
||||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||||
private static final String SVR2_MRENCLAVE = "0ff2d7d4efbe7cfc24ac069a16fba898928dbe6c40d500c8b6da55733c727d6e";
|
private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036";
|
||||||
private static final String SVR2_MRENCLAVE_LEGACY = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302";
|
|
||||||
|
|
||||||
private static final String URL = "https://chat.staging.signal.org";
|
private static final String URL = "https://chat.staging.signal.org";
|
||||||
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
||||||
@ -49,10 +48,10 @@ class StagingConfig {
|
|||||||
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
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==");
|
.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()
|
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()
|
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;
|
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
|
||||||
|
|
||||||
@ -94,7 +93,7 @@ class StagingConfig {
|
|||||||
createDefaultServiceConfiguration(interceptors),
|
createDefaultServiceConfiguration(interceptors),
|
||||||
getUnidentifiedSenderTrustRoots(),
|
getUnidentifiedSenderTrustRoots(),
|
||||||
CDSI_MRENCLAVE,
|
CDSI_MRENCLAVE,
|
||||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
List.of(SVR2_MRENCLAVE));
|
||||||
}
|
}
|
||||||
|
|
||||||
private StagingConfig() {
|
private StagingConfig() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.asamk.signal.manager.config;
|
package org.asamk.signal.manager.config;
|
||||||
|
|
||||||
import org.signal.network.config.TrustStore;
|
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
|||||||
@ -17,10 +17,8 @@ import org.asamk.signal.manager.util.Utils;
|
|||||||
import org.signal.core.models.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
import org.signal.core.util.Base64;
|
import org.signal.core.util.Base64;
|
||||||
import org.signal.core.util.crypto.DeviceNameCipher;
|
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.NoSessionException;
|
|
||||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
||||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||||
@ -33,7 +31,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
|
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||||
@ -41,6 +38,7 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
|||||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
|
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
|
||||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
|
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
|
||||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||||
@ -48,9 +46,7 @@ import org.whispersystems.signalservice.internal.push.SyncMessage;
|
|||||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -61,7 +57,6 @@ import okio.ByteString;
|
|||||||
|
|
||||||
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
|
|
||||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||||
|
|
||||||
public class AccountHelper {
|
public class AccountHelper {
|
||||||
@ -285,7 +280,7 @@ public class AccountHelper {
|
|||||||
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
|
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
|
||||||
pniChangeNumber);
|
pniChangeNumber);
|
||||||
encryptedDeviceMessages.add(message);
|
encryptedDeviceMessages.add(message);
|
||||||
} catch (UntrustedIdentityException | IOException | InvalidKeyException | NoSessionException e) {
|
} catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,14 +320,10 @@ public class AccountHelper {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePniChangeNumberMessage(selfChangeNumber, updatePni, false);
|
handlePniChangeNumberMessage(selfChangeNumber, updatePni);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handlePniChangeNumberMessage(
|
public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
|
||||||
final SyncMessage.PniChangeNumber pniChangeNumber,
|
|
||||||
final PNI updatedPni,
|
|
||||||
final boolean forcePniPreKeyRotation
|
|
||||||
) {
|
|
||||||
if (pniChangeNumber.identityKeyPair != null
|
if (pniChangeNumber.identityKeyPair != null
|
||||||
&& pniChangeNumber.registrationId != null
|
&& pniChangeNumber.registrationId != null
|
||||||
&& pniChangeNumber.signedPreKey != null) {
|
&& pniChangeNumber.signedPreKey != null) {
|
||||||
@ -346,19 +337,10 @@ public class AccountHelper {
|
|||||||
pniChangeNumber.lastResortKyberPreKey != null
|
pniChangeNumber.lastResortKyberPreKey != null
|
||||||
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
|
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
|
||||||
: null);
|
: null);
|
||||||
if (forcePniPreKeyRotation) {
|
|
||||||
try {
|
|
||||||
context.getPreKeyHelper().forceRefreshPreKeys(ServiceIdType.PNI);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Failed to force refresh PNI pre keys after PNI change sync", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed to handle change number message", e);
|
logger.warn("Failed to handle change number message", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final int USERNAME_MIN_LENGTH = 3;
|
public static final int USERNAME_MIN_LENGTH = 3;
|
||||||
@ -400,17 +382,13 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void reserveUsername(final List<Username> candidates) throws IOException {
|
private void reserveUsername(final List<Username> candidates) throws IOException {
|
||||||
final var candidateHashes = new ArrayList<byte[]>();
|
final var candidateHashes = new ArrayList<String>();
|
||||||
for (final var candidate : candidates) {
|
for (final var candidate : candidates) {
|
||||||
candidateHashes.add(candidate.getHash());
|
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
|
||||||
}
|
}
|
||||||
|
|
||||||
final var usernameHash = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||||
final var hashIndex = candidateHashes.stream()
|
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
||||||
.filter(candidateHash -> Arrays.equals(candidateHash, usernameHash))
|
|
||||||
.findFirst()
|
|
||||||
.map(candidateHashes::indexOf)
|
|
||||||
.orElse(-1);
|
|
||||||
if (hashIndex == -1) {
|
if (hashIndex == -1) {
|
||||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||||
throw new IOException("Unexpected username response");
|
throw new IOException("Unexpected username response");
|
||||||
@ -503,7 +481,8 @@ public class AccountHelper {
|
|||||||
final var usernameLink = account.getUsernameLink();
|
final var usernameLink = account.getUsernameLink();
|
||||||
|
|
||||||
if (usernameLink == null) {
|
if (usernameLink == null) {
|
||||||
handleResponseException(dependencies.getAccountApi().reserveUsername(List.of(username.getHash())));
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
|
||||||
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
||||||
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||||
account.setUsernameLink(linkComponents);
|
account.setUsernameLink(linkComponents);
|
||||||
@ -528,34 +507,28 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void deleteUsername() throws IOException {
|
public void deleteUsername() throws IOException {
|
||||||
handleResponseException(dependencies.getAccountApi().deleteUsernameHash());
|
handleResponseException(dependencies.getAccountApi().deleteUsername());
|
||||||
account.setUsernameLink(null);
|
account.setUsernameLink(null);
|
||||||
account.setUsername(null);
|
account.setUsername(null);
|
||||||
logger.debug("[deleteUsername] Successfully deleted the username.");
|
logger.debug("[deleteUsername] Successfully deleted the username.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeviceName(String deviceName) {
|
public void setDeviceName(String deviceName) {
|
||||||
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
account.setEncryptedDeviceName(Base64.encodeWithoutPadding(encryptedDeviceName));
|
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
||||||
|
account.setEncryptedDeviceName(encryptedDeviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
||||||
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
||||||
.setDeviceName(encryptedDeviceName, deviceId, cont));
|
handleResponseException(dependencies.getLinkDeviceApi().setDeviceName(encryptedDeviceName, deviceId));
|
||||||
context.getSyncHelper().sendDeviceNameChange(deviceId);
|
context.getSyncHelper().sendDeviceNameChange(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getEncryptedDeviceName(final String deviceName) {
|
|
||||||
final var identityKey = account.getAciIdentityKeyPair();
|
|
||||||
return DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), identityKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshDeviceName() throws IOException {
|
public void refreshDeviceName() throws IOException {
|
||||||
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
final var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||||
.getDevices(cont));
|
|
||||||
final var deviceId = account.getDeviceId();
|
final var deviceId = account.getDeviceId();
|
||||||
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
|
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
|
||||||
if (device.isPresent()) {
|
if (device.isPresent()) {
|
||||||
@ -589,39 +562,36 @@ public class AccountHelper {
|
|||||||
account.getOrCreatePinMasterKey(),
|
account.getOrCreatePinMasterKey(),
|
||||||
account.getOrCreateMediaRootBackupKey(),
|
account.getOrCreateMediaRootBackupKey(),
|
||||||
verificationCode.getVerificationCode(),
|
verificationCode.getVerificationCode(),
|
||||||
null,
|
null));
|
||||||
account.getAuthCredentialSalt()));
|
|
||||||
account.setMultiDevice(true);
|
account.setMultiDevice(true);
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeLinkedDevices(int deviceId) throws IOException {
|
public void removeLinkedDevices(int deviceId) throws IOException {
|
||||||
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi().removeDevice(deviceId, cont));
|
handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
|
||||||
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||||
.getDevices(cont));
|
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void migrateRegistrationPin() throws IOException {
|
public void migrateRegistrationPin() throws IOException {
|
||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
||||||
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void setRegistrationPin(String pin) throws IOException {
|
public void setRegistrationPin(String pin) throws IOException {
|
||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
||||||
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||||
|
|
||||||
account.setRegistrationLockPin(pin);
|
account.setRegistrationLockPin(pin);
|
||||||
updateAccountAttributes();
|
updateAccountAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void removeRegistrationPin() throws IOException {
|
public void removeRegistrationPin() throws IOException {
|
||||||
// Remove KBS Pin
|
// Remove KBS Pin
|
||||||
context.getPinHelper().removeRegistrationLockPin();
|
context.getPinHelper().removeRegistrationLockPin();
|
||||||
@ -634,7 +604,7 @@ public class AccountHelper {
|
|||||||
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
||||||
// If this is the primary device, other users can't send messages to this number anymore.
|
// If this is the primary device, other users can't send messages to this number anymore.
|
||||||
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
||||||
handleResponseExceptionSuspend(cont -> dependencies.getAccountApi().clearFcmToken(cont));
|
handleResponseException(dependencies.getAccountApi().clearFcmToken());
|
||||||
|
|
||||||
account.setRegistered(false);
|
account.setRegistered(false);
|
||||||
unregisteredListener.call();
|
unregisteredListener.call();
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
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.config.ServiceConfig;
|
||||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||||
import org.asamk.signal.manager.storage.AttachmentStore;
|
import org.asamk.signal.manager.storage.AttachmentStore;
|
||||||
@ -53,11 +52,9 @@ public class AttachmentHelper {
|
|||||||
|
|
||||||
public List<SignalServiceAttachment> uploadAttachments(
|
public List<SignalServiceAttachment> uploadAttachments(
|
||||||
final List<String> attachments,
|
final List<String> attachments,
|
||||||
final List<AttachmentDimensions> dimensions,
|
|
||||||
final List<String> blurHashes,
|
|
||||||
boolean voiceNote
|
boolean voiceNote
|
||||||
) throws AttachmentInvalidException, IOException {
|
) throws AttachmentInvalidException, IOException {
|
||||||
final var attachmentStreams = createAttachmentStreams(attachments, dimensions, blurHashes, voiceNote);
|
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload attachments here, so we only upload once even for multiple recipients
|
// Upload attachments here, so we only upload once even for multiple recipients
|
||||||
@ -74,31 +71,26 @@ public class AttachmentHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
|
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(
|
private List<SignalServiceAttachmentStream> createAttachmentStreams(
|
||||||
List<String> attachments,
|
List<String> attachments,
|
||||||
List<AttachmentDimensions> dimensions,
|
|
||||||
List<String> blurHashes,
|
|
||||||
boolean voiceNote
|
boolean voiceNote
|
||||||
) throws AttachmentInvalidException, IOException {
|
) throws AttachmentInvalidException, IOException {
|
||||||
if (attachments == null) {
|
if (attachments == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
||||||
for (var i = 0; i < attachments.size(); i++) {
|
for (var attachment : attachments) {
|
||||||
final var size = i < dimensions.size() ? dimensions.get(i) : null;
|
final var attachmentStream = getAttachmentStream(attachment, voiceNote);
|
||||||
final var blurHash = i < blurHashes.size() && !blurHashes.get(i).isEmpty() ? blurHashes.get(i) : null;
|
signalServiceAttachments.add(attachmentStream);
|
||||||
signalServiceAttachments.add(getAttachmentStream(attachments.get(i), size, blurHash, voiceNote));
|
|
||||||
}
|
}
|
||||||
return signalServiceAttachments;
|
return signalServiceAttachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceAttachmentStream getAttachmentStream(
|
private SignalServiceAttachmentStream getAttachmentStream(
|
||||||
final String attachment,
|
final String attachment,
|
||||||
final AttachmentDimensions dimensions,
|
|
||||||
final String blurHash,
|
|
||||||
final boolean voiceNote
|
final boolean voiceNote
|
||||||
) throws AttachmentInvalidException {
|
) throws AttachmentInvalidException {
|
||||||
try {
|
try {
|
||||||
@ -124,8 +116,6 @@ public class AttachmentHelper {
|
|||||||
return AttachmentUtils.createAttachmentStream(streamDetails,
|
return AttachmentUtils.createAttachmentStream(streamDetails,
|
||||||
streamDetailsAndFileName.second(),
|
streamDetailsAndFileName.second(),
|
||||||
voiceNote,
|
voiceNote,
|
||||||
dimensions,
|
|
||||||
blurHash,
|
|
||||||
uploadSpec);
|
uploadSpec);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new AttachmentInvalidException(attachment, e);
|
throw new AttachmentInvalidException(attachment, e);
|
||||||
@ -140,7 +130,7 @@ public class AttachmentHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
||||||
final var attachmentStream = getAttachmentStream(attachment, null, null, false);
|
final var attachmentStream = getAttachmentStream(attachment, false);
|
||||||
return uploadAttachment(attachmentStream);
|
return uploadAttachment(attachmentStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -649,7 +649,7 @@ public class CallManager implements AutoCloseable {
|
|||||||
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
|
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
|
||||||
default -> HangupMessage.Type.NORMAL;
|
default -> HangupMessage.Type.NORMAL;
|
||||||
};
|
};
|
||||||
var hangupMessage = new HangupMessage(state.callId, type, 0);
|
var hangupMessage = new HangupMessage(state.callId, type, state.deviceId);
|
||||||
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
|
||||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
|
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
|
||||||
@ -711,7 +711,7 @@ public class CallManager implements AutoCloseable {
|
|||||||
&& !"rejected".equals(reason)
|
&& !"rejected".equals(reason)
|
||||||
&& !"remote_busy".equals(reason)
|
&& !"remote_busy".equals(reason)
|
||||||
&& !"ringrtc_hangup".equals(reason)) {
|
&& !"ringrtc_hangup".equals(reason)) {
|
||||||
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, 0);
|
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, state.deviceId);
|
||||||
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
||||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
|
|||||||
@ -85,18 +85,12 @@ public class ContactHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setContactBlocked(RecipientId recipientId, boolean blocked) {
|
public void setContactBlocked(RecipientId recipientId, boolean blocked) {
|
||||||
setContactBlocked(recipientId, blocked, blocked ? System.currentTimeMillis() : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContactBlocked(RecipientId recipientId, boolean blocked, long blockedAt) {
|
|
||||||
var contact = account.getContactStore().getContact(recipientId);
|
var contact = account.getContactStore().getContact(recipientId);
|
||||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
if (blocked) {
|
if (blocked) {
|
||||||
builder.withIsProfileSharingEnabled(false);
|
builder.withIsProfileSharingEnabled(false);
|
||||||
}
|
}
|
||||||
account.getContactStore()
|
account.getContactStore().storeContact(recipientId, builder.withIsBlocked(blocked).build());
|
||||||
.storeContact(recipientId,
|
|
||||||
builder.withIsBlocked(blocked).withBlockedAt(blocked ? blockedAt : 0).build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
||||||
|
|||||||
@ -363,28 +363,6 @@ public class GroupHelper {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendGroupMessageResults terminateGroup(final GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
|
||||||
final var group = getGroupForUpdating(groupId);
|
|
||||||
if (!(group instanceof GroupInfoV2)) {
|
|
||||||
throw new IOException("Terminating a group is only supported for Signal group v2 groups.");
|
|
||||||
}
|
|
||||||
|
|
||||||
SendGroupMessageResults results;
|
|
||||||
try {
|
|
||||||
results = terminateGroupV2((GroupInfoV2) group);
|
|
||||||
} catch (ConflictException e) {
|
|
||||||
// Detected conflicting update, refreshing group and trying again
|
|
||||||
results = terminateGroupV2((GroupInfoV2) getGroup(groupId, true));
|
|
||||||
}
|
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendGroupMessageResults terminateGroupV2(final GroupInfoV2 group) throws IOException {
|
|
||||||
final var groupGroupChangePair = context.getGroupV2Helper().terminateGroup(group);
|
|
||||||
return sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException {
|
public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException {
|
||||||
var group = getGroupForUpdating(groupId);
|
var group = getGroupForUpdating(groupId);
|
||||||
|
|
||||||
@ -459,21 +437,12 @@ public class GroupHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
|
public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
|
||||||
setGroupBlocked(groupId, blocked, blocked ? System.currentTimeMillis() : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGroupBlocked(
|
|
||||||
final GroupId groupId,
|
|
||||||
final boolean blocked,
|
|
||||||
final long blockedAt
|
|
||||||
) throws GroupNotFoundException {
|
|
||||||
var group = getGroup(groupId);
|
var group = getGroup(groupId);
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
throw new GroupNotFoundException(groupId);
|
throw new GroupNotFoundException(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
group.setBlocked(blocked);
|
group.setBlocked(blocked);
|
||||||
group.setBlockedAt(blocked ? blockedAt : 0);
|
|
||||||
account.getGroupStore().updateGroup(group);
|
account.getGroupStore().updateGroup(group);
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -264,9 +264,6 @@ class GroupV2Helper {
|
|||||||
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
|
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
|
||||||
final var selfAci = getSelfAci();
|
final var selfAci = getSelfAci();
|
||||||
var selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, selfAci);
|
var selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, selfAci);
|
||||||
if (selfPendingMember.isEmpty()) {
|
|
||||||
selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, getSelfPni());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selfPendingMember.isPresent()) {
|
if (selfPendingMember.isPresent()) {
|
||||||
return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
|
return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
|
||||||
@ -543,14 +540,6 @@ class GroupV2Helper {
|
|||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pair<DecryptedGroup, GroupChangeResponse> terminateGroup(
|
|
||||||
GroupInfoV2 groupInfoV2
|
|
||||||
) throws IOException {
|
|
||||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
|
||||||
final var change = groupOperations.createTerminateGroup();
|
|
||||||
return commitChange(groupInfoV2, change);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair<DecryptedGroup, GroupChangeResponse> setMemberLabels(
|
Pair<DecryptedGroup, GroupChangeResponse> setMemberLabels(
|
||||||
GroupInfoV2 groupInfoV2,
|
GroupInfoV2 groupInfoV2,
|
||||||
String labelEmoji,
|
String labelEmoji,
|
||||||
@ -758,14 +747,8 @@ class GroupV2Helper {
|
|||||||
var authCredentialResponse = groupApiCredentials.get(todaySeconds);
|
var authCredentialResponse = groupApiCredentials.get(todaySeconds);
|
||||||
final var aci = getSelfAci();
|
final var aci = getSelfAci();
|
||||||
final var pni = getSelfPni();
|
final var pni = getSelfPni();
|
||||||
final var authCredentialSalt = context.getAccount().getAuthCredentialSalt();
|
|
||||||
return dependencies.getGroupsV2Api()
|
return dependencies.getGroupsV2Api()
|
||||||
.getGroupsV2AuthorizationString(aci,
|
.getGroupsV2AuthorizationString(aci, pni, todaySeconds, groupSecretParams, authCredentialResponse);
|
||||||
pni,
|
|
||||||
authCredentialSalt,
|
|
||||||
todaySeconds,
|
|
||||||
groupSecretParams,
|
|
||||||
authCredentialResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ACI getSelfAci() {
|
private ACI getSelfAci() {
|
||||||
|
|||||||
@ -340,9 +340,9 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (account.getPni().equals(destination.serviceId)) {
|
if (account.getPni().equals(destination.serviceId)) {
|
||||||
account.getRecipientStore().markNeedsPniSignature(sender, true);
|
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true);
|
||||||
} else if (account.getAci().equals(destination.serviceId)) {
|
} else if (account.getAci().equals(destination.serviceId)) {
|
||||||
account.getRecipientStore().markNeedsPniSignature(sender, false);
|
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.getReceiptMessage().isPresent()) {
|
if (content.getReceiptMessage().isPresent()) {
|
||||||
@ -498,11 +498,10 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
|
logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
|
||||||
final var recipientId = account.getRecipientTrustedResolver()
|
account.getRecipientTrustedResolver()
|
||||||
.resolveRecipientTrusted(Optional.of(ACI.from(aci.getRawUuid())),
|
.resolveRecipientTrusted(Optional.of(ACI.from(aci.getRawUuid())),
|
||||||
Optional.of(pni),
|
Optional.of(pni),
|
||||||
senderAddress.getNumber());
|
senderAddress.getNumber());
|
||||||
account.getRecipientStore().markPniSignatureVerified(recipientId);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -626,12 +625,13 @@ public final class IncomingMessageHandler {
|
|||||||
for (var individual : blockedListMessage.individuals) {
|
for (var individual : blockedListMessage.individuals) {
|
||||||
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
||||||
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
||||||
context.getContactHelper().setContactBlocked(recipientId, true, individual.getBlockedAt());
|
context.getContactHelper().setContactBlocked(recipientId, true);
|
||||||
}
|
}
|
||||||
for (var group : blockedListMessage.groups) {
|
for (var groupId : blockedListMessage.groupIds.stream()
|
||||||
final var groupId = GroupId.unknownVersion(group.getGroupId());
|
.map(GroupId::unknownVersion)
|
||||||
|
.collect(Collectors.toSet())) {
|
||||||
try {
|
try {
|
||||||
context.getGroupHelper().setGroupBlocked(groupId, true, group.getBlockedAt());
|
context.getGroupHelper().setGroupBlocked(groupId, true);
|
||||||
} catch (GroupNotFoundException e) {
|
} catch (GroupNotFoundException e) {
|
||||||
logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
|
logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
|
||||||
groupId.toBase64());
|
groupId.toBase64());
|
||||||
@ -717,30 +717,11 @@ public final class IncomingMessageHandler {
|
|||||||
}
|
}
|
||||||
if (syncMessage.getPniChangeNumber().isPresent()) {
|
if (syncMessage.getPniChangeNumber().isPresent()) {
|
||||||
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
||||||
if (account.isPrimaryDevice()) {
|
logger.debug("Received PNI change number sync message, applying.");
|
||||||
logger.warn("Received PNI change number sync message on primary device, ignoring.");
|
|
||||||
} 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();
|
final var updatedPniString = envelope.getUpdatedPni();
|
||||||
if (updatedPniString != null && !updatedPniString.isEmpty()) {
|
if (updatedPniString != null && !updatedPniString.isEmpty()) {
|
||||||
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
|
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
|
||||||
final var applied = context.getAccountHelper()
|
context.getAccountHelper().handlePniChangeNumberMessage(pniChangeNumber, updatedPni);
|
||||||
.handlePniChangeNumberMessage(pniChangeNumber, updatedPni, true);
|
|
||||||
if (applied) {
|
|
||||||
account.setLastAppliedPniChangeServerTimestamp(envelopeServerTimestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (syncMessage.getDeviceNameChange().isPresent()) {
|
if (syncMessage.getDeviceNameChange().isPresent()) {
|
||||||
@ -836,15 +817,6 @@ public final class IncomingMessageHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.isTerminated()) {
|
|
||||||
return message == null
|
|
||||||
|| message.getBody().isPresent()
|
|
||||||
|| message.getAttachments().isPresent()
|
|
||||||
|| message.getQuote().isPresent()
|
|
||||||
|| message.getPreviews().isPresent()
|
|
||||||
|| message.getMentions().isPresent()
|
|
||||||
|| message.getSticker().isPresent();
|
|
||||||
}
|
|
||||||
if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
|
if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
|
||||||
return message == null
|
return message == null
|
||||||
|| message.getBody().isPresent()
|
|| message.getBody().isPresent()
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import org.asamk.signal.manager.util.PaymentUtils;
|
|||||||
import org.asamk.signal.manager.util.ProfileUtils;
|
import org.asamk.signal.manager.util.ProfileUtils;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.signal.core.util.ExpiringProfileCredentialUtil;
|
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||||
@ -33,6 +32,7 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
|||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||||
|
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@ -103,16 +103,8 @@ public final class ProfileHelper {
|
|||||||
return getRecipientProfiles(recipientIds, false);
|
return getRecipientProfiles(recipientIds, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean refreshRecipientProfile(RecipientId recipientId) {
|
public void refreshRecipientProfile(RecipientId recipientId) {
|
||||||
try {
|
getRecipientProfile(recipientId, true);
|
||||||
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 refreshRecipientProfiles(Collection<RecipientId> recipientIds) {
|
public void refreshRecipientProfiles(Collection<RecipientId> recipientIds) {
|
||||||
@ -202,8 +194,10 @@ public final class ProfileHelper {
|
|||||||
final var streamDetails = avatar != null && avatar.isPresent()
|
final var streamDetails = avatar != null && avatar.isPresent()
|
||||||
? Utils.createStreamDetails(avatar.get())
|
? Utils.createStreamDetails(avatar.get())
|
||||||
.first()
|
.first()
|
||||||
: forceUploadAvatar && avatar == null ? context.getAvatarStore()
|
: forceUploadAvatar && avatar == null
|
||||||
.retrieveProfileAvatar(account.getSelfRecipientAddress()) : null;
|
? context.getAvatarStore()
|
||||||
|
.retrieveProfileAvatar(account.getSelfRecipientAddress())
|
||||||
|
: null;
|
||||||
try (streamDetails) {
|
try (streamDetails) {
|
||||||
final var avatarUploadParams = streamDetails != null
|
final var avatarUploadParams = streamDetails != null
|
||||||
? AvatarUploadParams.forAvatar(streamDetails)
|
? AvatarUploadParams.forAvatar(streamDetails)
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
|||||||
import org.signal.core.models.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.messages.EnvelopeResponse;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||||
@ -148,11 +147,8 @@ public class ReceiveHelper {
|
|||||||
logger.debug("Retrieved {} envelopes!", batch.size());
|
logger.debug("Retrieved {} envelopes!", batch.size());
|
||||||
isWaitingForMessage = false;
|
isWaitingForMessage = false;
|
||||||
for (final var it : batch) {
|
for (final var it : batch) {
|
||||||
if (it instanceof EnvelopeResponse.Unparseable) {
|
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
|
||||||
logger.warn("Received unparseable envelope from server, ignoring.");
|
it.getServerDeliveredTimestamp());
|
||||||
} else if (it instanceof EnvelopeResponse.Parsed parsed) {
|
|
||||||
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(parsed.getEnvelope(),
|
|
||||||
parsed.getServerDeliveredTimestamp());
|
|
||||||
final var sourceServiceId = envelope1.getSourceServiceId();
|
final var sourceServiceId = envelope1.getSourceServiceId();
|
||||||
final var recipientId = sourceServiceId == null
|
final var recipientId = sourceServiceId == null
|
||||||
? null
|
? null
|
||||||
@ -160,7 +156,6 @@ public class ReceiveHelper {
|
|||||||
logger.trace("Storing new message from {}", recipientId);
|
logger.trace("Storing new message from {}", recipientId);
|
||||||
// store message on disk, before acknowledging receipt to the server
|
// store message on disk, before acknowledging receipt to the server
|
||||||
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
signalWebSocket.sendAck(it);
|
signalWebSocket.sendAck(it);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -172,9 +167,6 @@ public class ReceiveHelper {
|
|||||||
backOffCounter = 0;
|
backOffCounter = 0;
|
||||||
|
|
||||||
if (queueNotEmpty) {
|
if (queueNotEmpty) {
|
||||||
if (cachedMessage[0] == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (remainingMessages > 0) {
|
if (remainingMessages > 0) {
|
||||||
remainingMessages -= 1;
|
remainingMessages -= 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,8 +35,6 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
|||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||||
@ -332,91 +330,6 @@ public class SendHelper {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a story message (file attachment) to "My Story".
|
|
||||||
*/
|
|
||||||
public List<SendMessageResult> sendStoryMessage(
|
|
||||||
SignalServiceStoryMessage storyMessage,
|
|
||||||
long timestamp,
|
|
||||||
Set<RecipientId> recipientIds,
|
|
||||||
boolean allowsReplies
|
|
||||||
) throws IOException {
|
|
||||||
final var messageSender = dependencies.getMessageSender();
|
|
||||||
|
|
||||||
final var recipientIdList = List.copyOf(recipientIds);
|
|
||||||
final var addressesMap = recipientIdList.stream()
|
|
||||||
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
|
||||||
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds);
|
|
||||||
|
|
||||||
final var addresses = recipientIdList.stream().map(addressesMap::get).toList();
|
|
||||||
final var unidentifiedAccesses = recipientIdList.stream().map(unidentifiedAccessesMap::get).toList();
|
|
||||||
final var storyMessageRecipients = recipientIdList.stream()
|
|
||||||
.map(id -> new SignalServiceStoryMessageRecipient(addressesMap.get(id),
|
|
||||||
List.of(DistributionId.MY_STORY.asUuid().toString()),
|
|
||||||
allowsReplies))
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
final List<SendMessageResult> results;
|
|
||||||
try {
|
|
||||||
results = messageSender.sendGroupStory(DistributionId.MY_STORY,
|
|
||||||
Optional.empty(),
|
|
||||||
addresses,
|
|
||||||
unidentifiedAccesses,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
storyMessage,
|
|
||||||
timestamp,
|
|
||||||
storyMessageRecipients,
|
|
||||||
null);
|
|
||||||
} catch (UntrustedIdentityException | InvalidKeyException | NoSessionException |
|
|
||||||
InvalidRegistrationIdException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var r : results) {
|
|
||||||
handleSendMessageResult(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a story message (file attachment) to a group.
|
|
||||||
*/
|
|
||||||
public List<SendMessageResult> sendGroupStoryMessage(
|
|
||||||
SignalServiceStoryMessage storyMessage,
|
|
||||||
long timestamp,
|
|
||||||
GroupInfoV2 groupInfo,
|
|
||||||
boolean allowsReplies
|
|
||||||
) throws IOException {
|
|
||||||
final var messageSender = dependencies.getMessageSender();
|
|
||||||
|
|
||||||
final var allRecipientIds = groupInfo.getMembersWithout(account.getSelfRecipientId());
|
|
||||||
|
|
||||||
final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupStory(
|
|
||||||
distId,
|
|
||||||
Optional.of(groupInfo.getMasterKey().serialize()),
|
|
||||||
recipients,
|
|
||||||
unidentifiedAccess,
|
|
||||||
groupSendEndorsements,
|
|
||||||
isRecipientUpdate,
|
|
||||||
storyMessage,
|
|
||||||
timestamp,
|
|
||||||
recipients.stream()
|
|
||||||
.map(address -> new SignalServiceStoryMessageRecipient(address,
|
|
||||||
List.of(groupInfo.getDistributionId().asUuid().toString()),
|
|
||||||
allowsReplies))
|
|
||||||
.collect(Collectors.toSet()),
|
|
||||||
null);
|
|
||||||
final var results = sendGroupMessageInternal(null, senderKeySender, allRecipientIds, groupInfo, false);
|
|
||||||
|
|
||||||
for (var r : results) {
|
|
||||||
handleSendMessageResult(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<SendMessageResult> sendAsGroupMessage(
|
private List<SendMessageResult> sendAsGroupMessage(
|
||||||
final SignalServiceDataMessage.Builder messageBuilder,
|
final SignalServiceDataMessage.Builder messageBuilder,
|
||||||
final GroupInfo g,
|
final GroupInfo g,
|
||||||
@ -567,7 +480,7 @@ public class SendHelper {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
|
||||||
var g = context.getGroupHelper().getGroup(groupId);
|
var g = context.getGroupHelper().getGroup(groupId);
|
||||||
if (g == null) {
|
if (g == null) {
|
||||||
throw new GroupNotFoundException(groupId);
|
throw new GroupNotFoundException(groupId);
|
||||||
@ -575,10 +488,6 @@ public class SendHelper {
|
|||||||
if (!g.isMember(account.getSelfRecipientId())) {
|
if (!g.isMember(account.getSelfRecipientId())) {
|
||||||
throw new NotAGroupMemberException(groupId, g.getTitle());
|
throw new NotAGroupMemberException(groupId, g.getTitle());
|
||||||
}
|
}
|
||||||
if (g.isTerminated()) {
|
|
||||||
// Other clients drop messages sent to a terminated group.
|
|
||||||
throw new GroupSendingNotAllowedException(groupId, g.getTitle());
|
|
||||||
}
|
|
||||||
if (!g.isProfileSharingEnabled()) {
|
if (!g.isProfileSharingEnabled()) {
|
||||||
g.setProfileSharingEnabled(true);
|
g.setProfileSharingEnabled(true);
|
||||||
account.getGroupStore().updateGroup(g);
|
account.getGroupStore().updateGroup(g);
|
||||||
@ -598,31 +507,9 @@ public class SendHelper {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Recipients that are already known to be unregistered are skipped here.
|
final var addressesMap = recipientIds.stream()
|
||||||
// Otherwise every group send re-attempts them via the slow legacy 1:1
|
|
||||||
// fan-out, which on large groups can take tens of seconds (and time out).
|
|
||||||
// The unregistered flag is maintained independently by profile/CDS
|
|
||||||
// discovery, which clears it once a recipient registers again, so they
|
|
||||||
// are re-included automatically. An unregisteredFailure result is still
|
|
||||||
// returned for each skipped recipient, so callers see them unchanged.
|
|
||||||
final var skippedResults = new ArrayList<SendMessageResult>();
|
|
||||||
final Set<RecipientId> targetRecipientIds;
|
|
||||||
final var unregisteredRecipientIds = account.getRecipientStore().getUnregisteredRecipientIds(recipientIds);
|
|
||||||
if (unregisteredRecipientIds.isEmpty()) {
|
|
||||||
targetRecipientIds = recipientIds;
|
|
||||||
} else {
|
|
||||||
logger.debug("Skipping {} known-unregistered recipient(s) in group send.", unregisteredRecipientIds.size());
|
|
||||||
targetRecipientIds = new HashSet<>(recipientIds);
|
|
||||||
targetRecipientIds.removeAll(unregisteredRecipientIds);
|
|
||||||
for (final var recipientId : unregisteredRecipientIds) {
|
|
||||||
skippedResults.add(SendMessageResult.unregisteredFailure(context.getRecipientHelper()
|
|
||||||
.resolveSignalServiceAddress(recipientId)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final var addressesMap = targetRecipientIds.stream()
|
|
||||||
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
||||||
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(targetRecipientIds);
|
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds);
|
||||||
final var groupSendEndorsementsResult = getGroupSendEndorsements(groupInfo);
|
final var groupSendEndorsementsResult = getGroupSendEndorsements(groupInfo);
|
||||||
final var groupSecretParams = groupInfo instanceof GroupInfoV2 gv2
|
final var groupSecretParams = groupInfo instanceof GroupInfoV2 gv2
|
||||||
? GroupSecretParams.deriveFromMasterKey((gv2.getMasterKey()))
|
? GroupSecretParams.deriveFromMasterKey((gv2.getMasterKey()))
|
||||||
@ -636,7 +523,7 @@ public class SendHelper {
|
|||||||
: groupSendEndorsementsResult.first();
|
: groupSendEndorsementsResult.first();
|
||||||
Set<RecipientId> senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null
|
Set<RecipientId> senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null
|
||||||
? Set.of()
|
? Set.of()
|
||||||
: targetRecipientIds.stream()
|
: recipientIds.stream()
|
||||||
.filter(s -> this.isSenderKeyCapable(s,
|
.filter(s -> this.isSenderKeyCapable(s,
|
||||||
addressesMap.get(s),
|
addressesMap.get(s),
|
||||||
unidentifiedAccessesMap.get(s),
|
unidentifiedAccessesMap.get(s),
|
||||||
@ -646,9 +533,7 @@ public class SendHelper {
|
|||||||
logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
|
logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
|
||||||
senderKeyTargets = Set.of();
|
senderKeyTargets = Set.of();
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Can use sender key for {}/{} recipients.",
|
logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
|
||||||
senderKeyTargets.size(),
|
|
||||||
targetRecipientIds.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
|
final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
|
||||||
@ -684,11 +569,11 @@ public class SendHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var legacyTargets = new HashSet<>(targetRecipientIds);
|
final var legacyTargets = new HashSet<>(recipientIds);
|
||||||
legacyTargets.removeAll(senderKeyTargets);
|
legacyTargets.removeAll(senderKeyTargets);
|
||||||
final boolean onlyTargetIsSelfWithLinkedDevice = targetRecipientIds.isEmpty() && account.isMultiDevice();
|
final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
|
||||||
|
|
||||||
if (legacySender != null && (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice)) {
|
if (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice) {
|
||||||
if (!legacyTargets.isEmpty()) {
|
if (!legacyTargets.isEmpty()) {
|
||||||
logger.debug("Need to do {} legacy sends.", legacyTargets.size());
|
logger.debug("Need to do {} legacy sends.", legacyTargets.size());
|
||||||
} else {
|
} else {
|
||||||
@ -720,7 +605,6 @@ public class SendHelper {
|
|||||||
isRecipientUpdate || !allResults.isEmpty());
|
isRecipientUpdate || !allResults.isEmpty());
|
||||||
allResults.addAll(results);
|
allResults.addAll(results);
|
||||||
}
|
}
|
||||||
allResults.addAll(skippedResults);
|
|
||||||
final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
|
final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
|
||||||
logger.debug("Sending took {}", duration.toString());
|
logger.debug("Sending took {}", duration.toString());
|
||||||
return allResults;
|
return allResults;
|
||||||
@ -783,7 +667,7 @@ public class SendHelper {
|
|||||||
final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
|
final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
|
||||||
logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, addresses.size());
|
logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, addresses.size());
|
||||||
return results;
|
return results;
|
||||||
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException | NoSessionException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -975,7 +859,7 @@ public class SendHelper {
|
|||||||
SignalServiceAddress address,
|
SignalServiceAddress address,
|
||||||
SealedSenderAccess unidentifiedAccess,
|
SealedSenderAccess unidentifiedAccess,
|
||||||
boolean includePniSignature
|
boolean includePniSignature
|
||||||
) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException, NoSessionException;
|
) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SenderKeySenderHandler {
|
interface SenderKeySenderHandler {
|
||||||
@ -995,6 +879,6 @@ public class SendHelper {
|
|||||||
List<SignalServiceAddress> recipients,
|
List<SignalServiceAddress> recipients,
|
||||||
List<SealedSenderAccess> unidentifiedAccess,
|
List<SealedSenderAccess> unidentifiedAccess,
|
||||||
boolean isRecipientUpdate
|
boolean isRecipientUpdate
|
||||||
) throws IOException, UntrustedIdentityException, NoSessionException;
|
) throws IOException, UntrustedIdentityException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,17 +4,13 @@ import org.asamk.signal.manager.api.GroupIdV1;
|
|||||||
import org.asamk.signal.manager.api.GroupIdV2;
|
import org.asamk.signal.manager.api.GroupIdV2;
|
||||||
import org.asamk.signal.manager.api.Pair;
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.asamk.signal.manager.api.Profile;
|
import org.asamk.signal.manager.api.Profile;
|
||||||
import org.asamk.signal.manager.api.StickerPackId;
|
|
||||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
|
||||||
import org.asamk.signal.manager.syncStorage.AccountRecordProcessor;
|
import org.asamk.signal.manager.syncStorage.AccountRecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.ContactRecordProcessor;
|
import org.asamk.signal.manager.syncStorage.ContactRecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor;
|
import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.GroupV2RecordProcessor;
|
import org.asamk.signal.manager.syncStorage.GroupV2RecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.StickerPackRecordProcessor;
|
|
||||||
import org.asamk.signal.manager.syncStorage.StorageSyncLoopDetector;
|
|
||||||
import org.asamk.signal.manager.syncStorage.StorageSyncModels;
|
import org.asamk.signal.manager.syncStorage.StorageSyncModels;
|
||||||
import org.asamk.signal.manager.syncStorage.StorageSyncValidations;
|
import org.asamk.signal.manager.syncStorage.StorageSyncValidations;
|
||||||
import org.asamk.signal.manager.syncStorage.WriteOperationResult;
|
import org.asamk.signal.manager.syncStorage.WriteOperationResult;
|
||||||
@ -46,7 +42,6 @@ import java.util.Collections;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
@ -57,19 +52,16 @@ public class StorageHelper {
|
|||||||
private static final List<Integer> KNOWN_TYPES = List.of(ManifestRecord.Identifier.Type.CONTACT.getValue(),
|
private static final List<Integer> KNOWN_TYPES = List.of(ManifestRecord.Identifier.Type.CONTACT.getValue(),
|
||||||
ManifestRecord.Identifier.Type.GROUPV1.getValue(),
|
ManifestRecord.Identifier.Type.GROUPV1.getValue(),
|
||||||
ManifestRecord.Identifier.Type.GROUPV2.getValue(),
|
ManifestRecord.Identifier.Type.GROUPV2.getValue(),
|
||||||
ManifestRecord.Identifier.Type.ACCOUNT.getValue(),
|
ManifestRecord.Identifier.Type.ACCOUNT.getValue());
|
||||||
ManifestRecord.Identifier.Type.STICKER_PACK.getValue());
|
|
||||||
|
|
||||||
private final SignalAccount account;
|
private final SignalAccount account;
|
||||||
private final SignalDependencies dependencies;
|
private final SignalDependencies dependencies;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final StorageSyncLoopDetector storageSyncLoopDetector;
|
|
||||||
|
|
||||||
public StorageHelper(final Context context) {
|
public StorageHelper(final Context context) {
|
||||||
this.account = context.getAccount();
|
this.account = context.getAccount();
|
||||||
this.dependencies = context.getDependencies();
|
this.dependencies = context.getDependencies();
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.storageSyncLoopDetector = new StorageSyncLoopDetector(account::isMultiDevice);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void syncDataWithStorage() throws IOException {
|
public void syncDataWithStorage() throws IOException {
|
||||||
@ -88,8 +80,6 @@ public class StorageHelper {
|
|||||||
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
||||||
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
||||||
localManifestVersion);
|
localManifestVersion);
|
||||||
final var fetchedRemoteManifest = result instanceof ManifestIfDifferentVersionResult.DifferentVersion;
|
|
||||||
final Set<StorageId> identityConflictsPendingRepair = new HashSet<>();
|
|
||||||
|
|
||||||
var needsForcePush = false;
|
var needsForcePush = false;
|
||||||
final var remoteManifest = switch (result) {
|
final var remoteManifest = switch (result) {
|
||||||
@ -118,22 +108,18 @@ public class StorageHelper {
|
|||||||
|
|
||||||
if (remoteManifest.version > localManifestVersion) {
|
if (remoteManifest.version > localManifestVersion) {
|
||||||
logger.trace("Remote version was newer, reading records.");
|
logger.trace("Remote version was newer, reading records.");
|
||||||
needsForcePush = readDataFromStorage(storageKey,
|
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest);
|
||||||
localManifest,
|
|
||||||
remoteManifest,
|
|
||||||
identityConflictsPendingRepair);
|
|
||||||
} else if (remoteManifest.version < localManifest.version) {
|
} else if (remoteManifest.version < localManifest.version) {
|
||||||
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
|
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
|
||||||
}
|
}
|
||||||
logger.trace("Done reading data from remote storage");
|
logger.trace("Done reading data from remote storage");
|
||||||
|
|
||||||
readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest, identityConflictsPendingRepair);
|
readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace("Adding missing storageIds to local data");
|
logger.trace("Adding missing storageIds to local data");
|
||||||
account.getRecipientStore().setMissingStorageIds();
|
account.getRecipientStore().setMissingStorageIds();
|
||||||
account.getGroupStore().setMissingStorageIds();
|
account.getGroupStore().setMissingStorageIds();
|
||||||
account.getStickerStore().setMissingStorageIds();
|
|
||||||
|
|
||||||
var needsMultiDeviceSync = false;
|
var needsMultiDeviceSync = false;
|
||||||
|
|
||||||
@ -155,11 +141,7 @@ public class StorageHelper {
|
|||||||
needsForcePush = true;
|
needsForcePush = true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
needsMultiDeviceSync = writeToStorage(storageKey,
|
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
||||||
remoteManifest,
|
|
||||||
needsForcePush,
|
|
||||||
fetchedRemoteManifest,
|
|
||||||
identityConflictsPendingRepair);
|
|
||||||
} catch (RetryLaterException e) {
|
} catch (RetryLaterException e) {
|
||||||
// TODO retry later
|
// TODO retry later
|
||||||
return;
|
return;
|
||||||
@ -204,8 +186,7 @@ public class StorageHelper {
|
|||||||
private boolean readDataFromStorage(
|
private boolean readDataFromStorage(
|
||||||
final StorageKey storageKey,
|
final StorageKey storageKey,
|
||||||
final SignalStorageManifest localManifest,
|
final SignalStorageManifest localManifest,
|
||||||
final SignalStorageManifest remoteManifest,
|
final SignalStorageManifest remoteManifest
|
||||||
final Set<StorageId> identityConflictsPendingRepair
|
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
var needsForcePush = false;
|
var needsForcePush = false;
|
||||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||||
@ -232,9 +213,7 @@ public class StorageHelper {
|
|||||||
remoteOnlyRecords.size());
|
remoteOnlyRecords.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
final var listListPair = processKnownRecords(connection,
|
final var listListPair = processKnownRecords(connection, remoteOnlyRecords);
|
||||||
remoteOnlyRecords,
|
|
||||||
identityConflictsPendingRepair);
|
|
||||||
final var unknownInserts = listListPair.first();
|
final var unknownInserts = listListPair.first();
|
||||||
final var updatedStorageIds = listListPair.second();
|
final var updatedStorageIds = listListPair.second();
|
||||||
final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds());
|
final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds());
|
||||||
@ -243,14 +222,11 @@ public class StorageHelper {
|
|||||||
final var updated = account.getRecipientStore()
|
final var updated = account.getRecipientStore()
|
||||||
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
||||||
oldUnregisteredLocalOnlyIds);
|
oldUnregisteredLocalOnlyIds);
|
||||||
final var updatedStickers = account.getStickerStore()
|
|
||||||
.removeStorageIdsFromLocalOnlyDeletedStickerPacks(connection, oldUnregisteredLocalOnlyIds);
|
|
||||||
|
|
||||||
if (updated > 0 || updatedStickers > 0) {
|
if (updated > 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Found {} recipients and {} sticker packs that were deleted remotely but only marked deleted locally. Removed those from local store.",
|
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
|
||||||
updated,
|
updated);
|
||||||
updatedStickers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,8 +253,7 @@ public class StorageHelper {
|
|||||||
|
|
||||||
private void readRecordsWithPreviouslyUnknownTypes(
|
private void readRecordsWithPreviouslyUnknownTypes(
|
||||||
final StorageKey storageKey,
|
final StorageKey storageKey,
|
||||||
final SignalStorageManifest remoteManifest,
|
final SignalStorageManifest remoteManifest
|
||||||
final Set<StorageId> identityConflictsPendingRepair
|
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||||
connection.setAutoCommit(false);
|
connection.setAutoCommit(false);
|
||||||
@ -292,8 +267,9 @@ public class StorageHelper {
|
|||||||
|
|
||||||
logger.debug("Found {} of the known-unknowns remotely.", remote.size());
|
logger.debug("Found {} of the known-unknowns remotely.", remote.size());
|
||||||
|
|
||||||
processKnownRecords(connection, remote, identityConflictsPendingRepair);
|
processKnownRecords(connection, remote);
|
||||||
account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, knownUnknownIds);
|
account.getUnknownStorageIdStore()
|
||||||
|
.deleteUnknownStorageIds(connection, remote.stream().map(SignalStorageRecord::getId).toList());
|
||||||
}
|
}
|
||||||
connection.commit();
|
connection.commit();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@ -304,9 +280,7 @@ public class StorageHelper {
|
|||||||
private boolean writeToStorage(
|
private boolean writeToStorage(
|
||||||
final StorageKey storageKey,
|
final StorageKey storageKey,
|
||||||
final SignalStorageManifest remoteManifest,
|
final SignalStorageManifest remoteManifest,
|
||||||
final boolean needsForcePush,
|
final boolean needsForcePush
|
||||||
final boolean fetchedRemoteManifest,
|
|
||||||
final Set<StorageId> identityConflictsPendingRepair
|
|
||||||
) throws IOException, RetryLaterException {
|
) throws IOException, RetryLaterException {
|
||||||
final WriteOperationResult remoteWriteOperation;
|
final WriteOperationResult remoteWriteOperation;
|
||||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||||
@ -316,8 +290,10 @@ public class StorageHelper {
|
|||||||
var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
|
var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
|
||||||
logger.debug("ID Difference :: {}", idDifference);
|
logger.debug("ID Difference :: {}", idDifference);
|
||||||
|
|
||||||
final var unknownStorageIds = account.getUnknownStorageIdStore().getUnknownStorageIds(connection);
|
final var unknownOnlyLocal = idDifference.localOnlyIds()
|
||||||
final var unknownOnlyLocal = findUnknownOnlyLocalStorageIds(idDifference.localOnlyIds(), unknownStorageIds);
|
.stream()
|
||||||
|
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (!unknownOnlyLocal.isEmpty()) {
|
if (!unknownOnlyLocal.isEmpty()) {
|
||||||
logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size());
|
logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size());
|
||||||
@ -342,28 +318,6 @@ public class StorageHelper {
|
|||||||
|
|
||||||
if (remoteWriteOperation.isEmpty()) {
|
if (remoteWriteOperation.isEmpty()) {
|
||||||
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
||||||
storageSyncLoopDetector.onConverged();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,18 +336,11 @@ public class StorageHelper {
|
|||||||
remoteWriteOperation.deletes());
|
remoteWriteOperation.deletes());
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case WriteStorageRecordsResult.ConflictError ignored -> {
|
case WriteStorageRecordsResult.ConflictError ignored -> {
|
||||||
storageSyncLoopDetector.onWriteFailed();
|
|
||||||
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||||
throw new RetryLaterException();
|
throw new RetryLaterException();
|
||||||
}
|
}
|
||||||
case WriteStorageRecordsResult.NetworkError networkError -> {
|
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
||||||
storageSyncLoopDetector.onWriteFailed();
|
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
||||||
throw networkError.getException();
|
|
||||||
}
|
|
||||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> {
|
|
||||||
storageSyncLoopDetector.onWriteFailed();
|
|
||||||
throw statusCodeError.getException();
|
|
||||||
}
|
|
||||||
case WriteStorageRecordsResult.Success ignored -> {
|
case WriteStorageRecordsResult.Success ignored -> {
|
||||||
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
||||||
storeManifestLocally(remoteWriteOperation.manifest());
|
storeManifestLocally(remoteWriteOperation.manifest());
|
||||||
@ -403,22 +350,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(
|
private void forcePushToStorage(
|
||||||
final StorageKey storageServiceKey
|
final StorageKey storageServiceKey
|
||||||
) throws IOException, RetryLaterException {
|
) throws IOException, RetryLaterException {
|
||||||
@ -435,7 +366,6 @@ public class StorageHelper {
|
|||||||
final Map<RecipientId, StorageId> newContactStorageIds;
|
final Map<RecipientId, StorageId> newContactStorageIds;
|
||||||
final Map<GroupIdV1, StorageId> newGroupV1StorageIds;
|
final Map<GroupIdV1, StorageId> newGroupV1StorageIds;
|
||||||
final Map<GroupIdV2, StorageId> newGroupV2StorageIds;
|
final Map<GroupIdV2, StorageId> newGroupV2StorageIds;
|
||||||
final Map<StickerPackId, StorageId> newStickerPackStorageIds;
|
|
||||||
|
|
||||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||||
connection.setAutoCommit(false);
|
connection.setAutoCommit(false);
|
||||||
@ -482,19 +412,6 @@ public class StorageHelper {
|
|||||||
new StorageRecord.Builder().groupV2(record).build()));
|
new StorageRecord.Builder().groupV2(record).build()));
|
||||||
}
|
}
|
||||||
|
|
||||||
final var stickerPacks = account.getStickerStore()
|
|
||||||
.getStickerPacks(connection)
|
|
||||||
.stream()
|
|
||||||
.filter(pack -> pack.storageId() != null)
|
|
||||||
.toList();
|
|
||||||
newStickerPackStorageIds = generateStickerPackStorageIds(stickerPacks);
|
|
||||||
for (final var stickerPack : stickerPacks) {
|
|
||||||
final var storageId = newStickerPackStorageIds.get(stickerPack.packId());
|
|
||||||
final var record = StorageSyncModels.localToRemoteRecord(stickerPack);
|
|
||||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
|
||||||
new StorageRecord.Builder().stickerPack(record).build()));
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.commit();
|
connection.commit();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException("Failed to sync remote storage", e);
|
throw new RuntimeException("Failed to sync remote storage", e);
|
||||||
@ -545,7 +462,6 @@ public class StorageHelper {
|
|||||||
connection.setAutoCommit(false);
|
connection.setAutoCommit(false);
|
||||||
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
|
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
|
||||||
account.getGroupStore().updateStorageIds(connection, newGroupV1StorageIds, newGroupV2StorageIds);
|
account.getGroupStore().updateStorageIds(connection, newGroupV1StorageIds, newGroupV2StorageIds);
|
||||||
account.getStickerStore().updateStorageIds(connection, newStickerPackStorageIds);
|
|
||||||
|
|
||||||
// delete all unknown storage ids
|
// delete all unknown storage ids
|
||||||
account.getUnknownStorageIdStore().deleteAllUnknownStorageIds(connection);
|
account.getUnknownStorageIdStore().deleteAllUnknownStorageIds(connection);
|
||||||
@ -578,14 +494,6 @@ public class StorageHelper {
|
|||||||
_ -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
_ -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<StickerPackId, StorageId> generateStickerPackStorageIds(
|
|
||||||
final List<StickerPack> stickerPacks
|
|
||||||
) {
|
|
||||||
return stickerPacks.stream()
|
|
||||||
.collect(Collectors.toMap(stickerPack -> stickerPack.packId(),
|
|
||||||
_ -> StorageId.forStickerPack(KeyUtils.createRawStorageId())));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void storeManifestLocally(
|
private void storeManifestLocally(
|
||||||
final SignalStorageManifest remoteManifest
|
final SignalStorageManifest remoteManifest
|
||||||
) {
|
) {
|
||||||
@ -625,7 +533,6 @@ public class StorageHelper {
|
|||||||
storageIds.addAll(account.getUnknownStorageIdStore().getUnknownStorageIds(connection));
|
storageIds.addAll(account.getUnknownStorageIdStore().getUnknownStorageIds(connection));
|
||||||
storageIds.addAll(account.getGroupStore().getStorageIds(connection));
|
storageIds.addAll(account.getGroupStore().getStorageIds(connection));
|
||||||
storageIds.addAll(account.getRecipientStore().getStorageIds(connection));
|
storageIds.addAll(account.getRecipientStore().getStorageIds(connection));
|
||||||
storageIds.addAll(account.getStickerStore().getStorageIds(connection));
|
|
||||||
storageIds.add(account.getRecipientStore().getSelfStorageId(connection));
|
storageIds.add(account.getRecipientStore().getSelfStorageId(connection));
|
||||||
return storageIds;
|
return storageIds;
|
||||||
}
|
}
|
||||||
@ -674,14 +581,6 @@ public class StorageHelper {
|
|||||||
account.getUsernameLink());
|
account.getUsernameLink());
|
||||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
|
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
|
||||||
}
|
}
|
||||||
case ManifestRecord.Identifier.Type.STICKER_PACK -> {
|
|
||||||
final var stickerPack = account.getStickerStore().getStickerPack(connection, storageId);
|
|
||||||
if (stickerPack == null) {
|
|
||||||
throw new AssertionError("Missing local sticker pack model for storage id: " + storageId);
|
|
||||||
}
|
|
||||||
final var record = StorageSyncModels.localToRemoteRecord(stickerPack);
|
|
||||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().stickerPack(record).build());
|
|
||||||
}
|
|
||||||
case null, default -> {
|
case null, default -> {
|
||||||
throw new AssertionError("Got unknown local storage record type: " + storageId);
|
throw new AssertionError("Got unknown local storage record type: " + storageId);
|
||||||
}
|
}
|
||||||
@ -738,8 +637,7 @@ public class StorageHelper {
|
|||||||
|
|
||||||
private Pair<List<StorageId>, List<StorageId>> processKnownRecords(
|
private Pair<List<StorageId>, List<StorageId>> processKnownRecords(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
List<SignalStorageRecord> records,
|
List<SignalStorageRecord> records
|
||||||
final Set<StorageId> identityConflictsPendingRepair
|
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var unknownRecords = new ArrayList<StorageId>();
|
final var unknownRecords = new ArrayList<StorageId>();
|
||||||
final var processedRecords = new ArrayList<StorageId>();
|
final var processedRecords = new ArrayList<StorageId>();
|
||||||
@ -747,18 +645,7 @@ public class StorageHelper {
|
|||||||
final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor());
|
final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor());
|
||||||
final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection);
|
final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection);
|
||||||
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
|
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
|
||||||
final var contactRecordProcessor = new ContactRecordProcessor(account,
|
final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
|
||||||
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) {
|
for (final var record : records) {
|
||||||
if (record.getProto().account != null) {
|
if (record.getProto().account != null) {
|
||||||
@ -777,10 +664,6 @@ public class StorageHelper {
|
|||||||
logger.debug("Reading record {} of type contact", record.getId());
|
logger.debug("Reading record {} of type contact", record.getId());
|
||||||
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
||||||
record.getId()));
|
record.getId()));
|
||||||
} else if (record.getProto().stickerPack != null) {
|
|
||||||
logger.debug("Reading record {} of type stickerPack", record.getId());
|
|
||||||
stickerPackRecordProcessor.process(StorageRecordConvertersKt.toSignalStickerPackRecord(record.getProto().stickerPack,
|
|
||||||
record.getId()));
|
|
||||||
} else {
|
} else {
|
||||||
unknownRecords.add(record.getId());
|
unknownRecords.add(record.getId());
|
||||||
}
|
}
|
||||||
@ -789,7 +672,6 @@ public class StorageHelper {
|
|||||||
processedRecords.addAll(groupV1RecordProcessor.getUpdatedStorageIds());
|
processedRecords.addAll(groupV1RecordProcessor.getUpdatedStorageIds());
|
||||||
processedRecords.addAll(groupV2RecordProcessor.getUpdatedStorageIds());
|
processedRecords.addAll(groupV2RecordProcessor.getUpdatedStorageIds());
|
||||||
processedRecords.addAll(contactRecordProcessor.getUpdatedStorageIds());
|
processedRecords.addAll(contactRecordProcessor.getUpdatedStorageIds());
|
||||||
processedRecords.addAll(stickerPackRecordProcessor.getUpdatedStorageIds());
|
|
||||||
|
|
||||||
return new Pair<>(unknownRecords, processedRecords);
|
return new Pair<>(unknownRecords, processedRecords);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -237,19 +237,18 @@ public class SyncHelper {
|
|||||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
||||||
if (address.aci().isPresent() || address.number().isPresent()) {
|
if (address.aci().isPresent() || address.number().isPresent()) {
|
||||||
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
||||||
address.number().orElse(null),
|
address.number().orElse(null)));
|
||||||
record.second().blockedAt()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var groups = new ArrayList<BlockedListMessage.Group>();
|
var groupIds = new ArrayList<byte[]>();
|
||||||
for (var record : account.getGroupStore().getGroups()) {
|
for (var record : account.getGroupStore().getGroups()) {
|
||||||
if (record.isBlocked()) {
|
if (record.isBlocked()) {
|
||||||
groups.add(new BlockedListMessage.Group(record.getGroupId().serialize(), record.getBlockedAt()));
|
groupIds.add(record.getGroupId().serialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return context.getSendHelper()
|
return context.getSendHelper()
|
||||||
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groups)));
|
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendVerifiedMessage(
|
public SendMessageResult sendVerifiedMessage(
|
||||||
|
|||||||
@ -83,7 +83,6 @@ import org.asamk.signal.manager.storage.AttachmentStore;
|
|||||||
import org.asamk.signal.manager.storage.AvatarStore;
|
import org.asamk.signal.manager.storage.AvatarStore;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
|
||||||
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
@ -98,23 +97,17 @@ import org.asamk.signal.manager.util.StickerUtils;
|
|||||||
import org.signal.core.models.ServiceId;
|
import org.signal.core.models.ServiceId;
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
|
import org.signal.core.util.Base64;
|
||||||
import org.signal.core.util.Hex;
|
import org.signal.core.util.Hex;
|
||||||
import org.signal.core.util.crypto.DeviceName;
|
|
||||||
import org.signal.core.util.crypto.DeviceNameCipher;
|
|
||||||
import org.signal.libsignal.net.LinkedDevice;
|
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.signal.libsignal.protocol.NoSessionException;
|
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||||
@ -122,8 +115,10 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
|||||||
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
|
||||||
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
import org.whispersystems.signalservice.internal.util.Util;
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
@ -160,7 +155,6 @@ import okio.Utf8;
|
|||||||
|
|
||||||
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
|
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
|
|
||||||
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
|
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
|
||||||
|
|
||||||
public class ManagerImpl implements Manager {
|
public class ManagerImpl implements Manager {
|
||||||
@ -250,11 +244,6 @@ public class ManagerImpl implements Manager {
|
|||||||
return account.getNumber();
|
return account.getNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSelfACI() {
|
|
||||||
return account.getAci().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkAccountState() throws IOException {
|
public void checkAccountState() throws IOException {
|
||||||
context.getAccountHelper().checkAccountState();
|
context.getAccountHelper().checkAccountState();
|
||||||
final var lastRecipientsRefresh = account.getLastRecipientsRefresh();
|
final var lastRecipientsRefresh = account.getLastRecipientsRefresh();
|
||||||
@ -484,26 +473,23 @@ public class ManagerImpl implements Manager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Device> getLinkedDevices() throws IOException {
|
public List<Device> getLinkedDevices() throws IOException {
|
||||||
final List<LinkedDevice> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||||
.getDevices(cont));
|
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
var identityKey = account.getAciIdentityKeyPair();
|
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
return devices.stream().map(d -> {
|
return devices.stream().map(d -> {
|
||||||
String deviceName = null;
|
String deviceName = d.getName();
|
||||||
if (d.getEncryptedName() != null && d.getEncryptedName().length > 0) {
|
if (deviceName != null) {
|
||||||
try {
|
try {
|
||||||
deviceName = new String(DeviceNameCipher.decryptDeviceName(DeviceName.ADAPTER.decode(d.getEncryptedName()),
|
deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
|
||||||
identityKey), StandardCharsets.UTF_8);
|
} catch (IOException e) {
|
||||||
} catch (Exception e) {
|
|
||||||
logger.debug("Failed to decrypt device name, maybe plain text?", e);
|
logger.debug("Failed to decrypt device name, maybe plain text?", e);
|
||||||
deviceName = new String(d.getEncryptedName(), StandardCharsets.UTF_8);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final var createdAt = getPlaintextCreatedAt(d);
|
final var createdAt = getPlaintextCreatedAt(d);
|
||||||
return new Device(d.getId(),
|
return new Device(d.getId(),
|
||||||
deviceName,
|
deviceName,
|
||||||
createdAt == null ? 0 : createdAt,
|
createdAt == null ? 0 : createdAt,
|
||||||
d.getLastSeen().toEpochMilli(),
|
d.getLastSeen(),
|
||||||
d.getId() == account.getDeviceId());
|
d.getId() == account.getDeviceId());
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
@ -523,7 +509,7 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getPlaintextCreatedAt(LinkedDevice d) {
|
private Long getPlaintextCreatedAt(DeviceInfo d) {
|
||||||
final var DECRYPTION_INFO = "deviceCreatedAt";
|
final var DECRYPTION_INFO = "deviceCreatedAt";
|
||||||
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
try {
|
try {
|
||||||
@ -531,9 +517,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var associatedData = new ByteArrayOutputStream();
|
var associatedData = new ByteArrayOutputStream();
|
||||||
associatedData.write(d.getId());
|
associatedData.write(d.getId());
|
||||||
associatedData.write(ByteBuffer.allocate(4).putInt(d.getRegistrationId()).array());
|
associatedData.write(ByteBuffer.allocate(4).putInt(d.getRegistrationId()).array());
|
||||||
var createdAtPlaintext = identityKey.open(d.getCreatedAtCiphertext(),
|
var createdAtPlaintext = identityKey.open(Base64.decode(d.getCreatedAtCiphertext()
|
||||||
DECRYPTION_INFO,
|
.getBytes(StandardCharsets.UTF_8)), DECRYPTION_INFO, associatedData.toByteArray());
|
||||||
associatedData.toByteArray());
|
|
||||||
return ByteBuffer.wrap(createdAtPlaintext).getLong();
|
return ByteBuffer.wrap(createdAtPlaintext).getLong();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed while reading the protobuf.", e);
|
logger.warn("Failed while reading the protobuf.", e);
|
||||||
@ -610,11 +595,6 @@ public class ManagerImpl implements Manager {
|
|||||||
context.getGroupHelper().deleteGroup(groupId);
|
context.getGroupHelper().deleteGroup(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public SendGroupMessageResults terminateGroup(GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
|
||||||
return context.getGroupHelper().terminateGroup(groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
String name,
|
String name,
|
||||||
@ -715,7 +695,10 @@ public class ManagerImpl implements Manager {
|
|||||||
)) {
|
)) {
|
||||||
final var result = notifySelf
|
final var result = notifySelf
|
||||||
? context.getSendHelper()
|
? context.getSendHelper()
|
||||||
.sendMessage(messageBuilder, account.getSelfRecipientId(), editTargetTimestamp, urgent)
|
.sendMessage(messageBuilder,
|
||||||
|
account.getSelfRecipientId(),
|
||||||
|
editTargetTimestamp,
|
||||||
|
urgent)
|
||||||
: context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
|
: context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
|
||||||
results.put(recipient, List.of(toSendMessageResult(result)));
|
results.put(recipient, List.of(toSendMessageResult(result)));
|
||||||
} else if (recipient instanceof RecipientIdentifier.Single single) {
|
} else if (recipient instanceof RecipientIdentifier.Single single) {
|
||||||
@ -803,20 +786,13 @@ public class ManagerImpl implements Manager {
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
|
final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
|
||||||
List<SendMessageResult> results;
|
|
||||||
if (receiptMessage.isDeliveryReceipt() || !Boolean.FALSE.equals(account.getConfigurationStore()
|
|
||||||
.getReadReceipts())) {
|
|
||||||
final var result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
|
final var result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
|
||||||
results = List.of(toSendMessageResult(result));
|
|
||||||
} else {
|
|
||||||
results = List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
final var aci = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).aci();
|
final var aci = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).aci();
|
||||||
if (aci.isPresent()) {
|
if (aci.isPresent()) {
|
||||||
context.getSyncHelper().sendSyncReceiptMessage(aci.get(), receiptMessage);
|
context.getSyncHelper().sendSyncReceiptMessage(aci.get(), receiptMessage);
|
||||||
}
|
}
|
||||||
return new SendMessageResults(timestamp, Map.of(sender, results));
|
return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result))));
|
||||||
} catch (UnregisteredRecipientException e) {
|
} catch (UnregisteredRecipientException e) {
|
||||||
return new SendMessageResults(timestamp,
|
return new SendMessageResults(timestamp,
|
||||||
Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress()))));
|
Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress()))));
|
||||||
@ -850,98 +826,6 @@ public class ManagerImpl implements Manager {
|
|||||||
return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp), message.urgent());
|
return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp), message.urgent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public SendMessageResults sendStory(
|
|
||||||
String attachment,
|
|
||||||
boolean allowsReplies,
|
|
||||||
Optional<GroupId> groupId
|
|
||||||
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException {
|
|
||||||
final var file = new File(attachment);
|
|
||||||
final var mimeType = MimeUtils.getFileMimeType(file);
|
|
||||||
if (mimeType.isEmpty() || (!mimeType.get().startsWith("image/") && !mimeType.get().startsWith("video/"))) {
|
|
||||||
throw new AttachmentInvalidException(attachment,
|
|
||||||
new IOException("Stories only support image and video attachments"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupId.isPresent()) {
|
|
||||||
return sendGroupStory(attachment, allowsReplies, groupId.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
final var recipients = account.getRecipientStore()
|
|
||||||
.getRecipients(true, Optional.of(false), Set.of(), Optional.empty());
|
|
||||||
final var recipientIds = recipients.stream()
|
|
||||||
.filter(r -> !r.getRecipientId().equals(account.getSelfRecipientId()))
|
|
||||||
.filter(r -> r.getContact() != null && !r.getContact().hideStory())
|
|
||||||
.map(r -> r.getRecipientId())
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
if (recipientIds.isEmpty()) {
|
|
||||||
throw new IOException("No eligible contacts found for story delivery");
|
|
||||||
}
|
|
||||||
|
|
||||||
final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment);
|
|
||||||
final var storyMessage = SignalServiceStoryMessage.forFileAttachment(account.getProfileKey().serialize(),
|
|
||||||
null,
|
|
||||||
uploadedAttachment,
|
|
||||||
allowsReplies,
|
|
||||||
List.of());
|
|
||||||
final var timestamp = getNextMessageTimestamp();
|
|
||||||
|
|
||||||
final var sendResults = context.getSendHelper()
|
|
||||||
.sendStoryMessage(storyMessage, timestamp, recipientIds, allowsReplies);
|
|
||||||
|
|
||||||
final var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
|
||||||
for (final var sendResult : sendResults) {
|
|
||||||
final var result = toSendMessageResult(sendResult);
|
|
||||||
results.put(RecipientIdentifier.Single.fromAddress(result.address()), List.of(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SendMessageResults(timestamp, results);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendMessageResults sendGroupStory(
|
|
||||||
String attachment,
|
|
||||||
boolean allowsReplies,
|
|
||||||
GroupId groupId
|
|
||||||
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException {
|
|
||||||
final var groupInfo = context.getGroupHelper().getGroup(groupId);
|
|
||||||
if (groupInfo == null) {
|
|
||||||
throw new GroupNotFoundException(groupId);
|
|
||||||
}
|
|
||||||
if (!groupInfo.isMember(account.getSelfRecipientId())) {
|
|
||||||
throw new NotAGroupMemberException(groupId, groupInfo.getTitle());
|
|
||||||
}
|
|
||||||
if (!(groupInfo instanceof GroupInfoV2 groupInfoV2)) {
|
|
||||||
throw new IOException("Stories are only supported for V2 groups");
|
|
||||||
}
|
|
||||||
if (groupInfoV2.isTerminated()) {
|
|
||||||
// Other clients drop messages sent to a terminated group.
|
|
||||||
throw new IOException("Cannot send a story to a group that has been terminated");
|
|
||||||
}
|
|
||||||
|
|
||||||
final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment);
|
|
||||||
final var groupContext = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
|
|
||||||
.withRevision(groupInfoV2.getGroup() == null ? 0 : groupInfoV2.getGroup().revision)
|
|
||||||
.build();
|
|
||||||
final var storyMessage = SignalServiceStoryMessage.forFileAttachment(account.getProfileKey().serialize(),
|
|
||||||
groupContext,
|
|
||||||
uploadedAttachment,
|
|
||||||
allowsReplies,
|
|
||||||
List.of());
|
|
||||||
final var timestamp = getNextMessageTimestamp();
|
|
||||||
|
|
||||||
final var sendResults = context.getSendHelper()
|
|
||||||
.sendGroupStoryMessage(storyMessage, timestamp, groupInfoV2, allowsReplies);
|
|
||||||
|
|
||||||
final var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
|
||||||
for (final var sendResult : sendResults) {
|
|
||||||
final var result = toSendMessageResult(sendResult);
|
|
||||||
results.put(RecipientIdentifier.Single.fromAddress(result.address()), List.of(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SendMessageResults(timestamp, results);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyMessage(
|
private void applyMessage(
|
||||||
final SignalServiceDataMessage.Builder messageBuilder,
|
final SignalServiceDataMessage.Builder messageBuilder,
|
||||||
final Message message
|
final Message message
|
||||||
@ -970,10 +854,7 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
if (!message.attachments().isEmpty()) {
|
if (!message.attachments().isEmpty()) {
|
||||||
final var uploadedAttachments = context.getAttachmentHelper()
|
final var uploadedAttachments = context.getAttachmentHelper()
|
||||||
.uploadAttachments(message.attachments(),
|
.uploadAttachments(message.attachments(), message.voiceNote());
|
||||||
message.attachmentDimensions(),
|
|
||||||
message.attachmentBlurHashes(),
|
|
||||||
message.voiceNote());
|
|
||||||
if (!additionalAttachments.isEmpty()) {
|
if (!additionalAttachments.isEmpty()) {
|
||||||
additionalAttachments.addAll(uploadedAttachments);
|
additionalAttachments.addAll(uploadedAttachments);
|
||||||
messageBuilder.withAttachments(additionalAttachments);
|
messageBuilder.withAttachments(additionalAttachments);
|
||||||
@ -1715,16 +1596,9 @@ public class ManagerImpl implements Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final var contact = account.getContactStore().getContact(recipientId);
|
final var contact = account.getContactStore().getContact(recipientId);
|
||||||
if (contact != null) {
|
if (contact != null && !Util.isEmpty(contact.getName())) {
|
||||||
final var nickname = contact.getDisplayNickname();
|
|
||||||
if (!Util.isEmpty(nickname)) {
|
|
||||||
return nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Util.isEmpty(contact.getName())) {
|
|
||||||
return contact.getName();
|
return contact.getName();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
|
final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
@ -1944,10 +1818,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, null);
|
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1963,10 +1835,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, null);
|
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1982,10 +1852,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, null);
|
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2008,10 +1876,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2026,10 +1892,8 @@ public class ManagerImpl implements Manager {
|
|||||||
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, null);
|
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, null);
|
||||||
try {
|
try {
|
||||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
throw new IOException("Untrusted identity for call recipient", e);
|
throw new IOException("Untrusted identity for call recipient", e);
|
||||||
} catch (NoSessionException e) {
|
|
||||||
throw new IOException("No session for call recipient", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import org.asamk.signal.manager.RegistrationManager;
|
|||||||
import org.asamk.signal.manager.SignalAccountFiles;
|
import org.asamk.signal.manager.SignalAccountFiles;
|
||||||
import org.asamk.signal.manager.api.AccountCheckException;
|
import org.asamk.signal.manager.api.AccountCheckException;
|
||||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
import org.asamk.signal.manager.api.NotRegisteredException;
|
||||||
import org.signal.core.util.UuidUtil;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -96,49 +95,25 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Manager getManager(final String identifier) {
|
public Manager getManager(final String number) {
|
||||||
synchronized (managers) {
|
synchronized (managers) {
|
||||||
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
final var manager = managers.stream()
|
||||||
// Check if UUID corresponds to an already-loaded manager
|
.filter(m -> m.getSelfNumber().equals(number))
|
||||||
final var existing = managers.stream()
|
|
||||||
.filter(m -> m.getSelfACI().equals(identifier))
|
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (existing != null) {
|
if (manager != null) {
|
||||||
logger.debug("Found already loaded manager for ACI: {}", identifier);
|
return manager;
|
||||||
return existing;
|
|
||||||
}
|
}
|
||||||
// Load by ACI
|
|
||||||
try {
|
try {
|
||||||
final var newManager = signalAccountFiles.initManagerByAci(identifier);
|
final var newManager = signalAccountFiles.initManager(number);
|
||||||
managers.add(newManager);
|
managers.add(newManager);
|
||||||
return newManager;
|
return newManager;
|
||||||
} catch (NotRegisteredException e) {
|
} catch (IOException | NotRegisteredException | AccountCheckException e) {
|
||||||
logger.debug("Manager not found by ACI: {}", identifier);
|
logger.warn("Failed to load new manager", e);
|
||||||
} catch (IOException | IllegalArgumentException | AccountCheckException e) {
|
|
||||||
logger.warn("Failed to load new manager by ACI: {}", identifier, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Phone number — check already loaded managers
|
|
||||||
var existing = managers.stream()
|
|
||||||
.filter(m -> m.getSelfNumber().equals(identifier))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
if (existing != null) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
// Load by phone number
|
|
||||||
try {
|
|
||||||
final var newManager = signalAccountFiles.initManagerByNumber(identifier);
|
|
||||||
managers.add(newManager);
|
|
||||||
return newManager;
|
|
||||||
} catch (NotRegisteredException | IOException | IllegalArgumentException | AccountCheckException e) {
|
|
||||||
logger.warn("Failed to load manager by number: {}", identifier, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI getNewProvisioningDeviceLinkUri() throws TimeoutException, IOException {
|
public URI getNewProvisioningDeviceLinkUri() throws TimeoutException, IOException {
|
||||||
|
|||||||
@ -19,57 +19,34 @@ package org.asamk.signal.manager.internal;
|
|||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.asamk.signal.manager.ProvisioningManager;
|
import org.asamk.signal.manager.ProvisioningManager;
|
||||||
import org.asamk.signal.manager.Settings;
|
import org.asamk.signal.manager.Settings;
|
||||||
|
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||||
import org.asamk.signal.manager.api.UserAlreadyExistsException;
|
import org.asamk.signal.manager.api.UserAlreadyExistsException;
|
||||||
import org.asamk.signal.manager.config.ServiceConfig;
|
import org.asamk.signal.manager.config.ServiceConfig;
|
||||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.signal.core.models.AccountEntropyPool;
|
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
|
||||||
import org.signal.core.models.backup.MediaRootBackupKey;
|
|
||||||
import org.signal.core.util.crypto.DeviceNameCipher;
|
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
|
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
|
||||||
import org.whispersystems.signalservice.api.account.DeviceAttributes;
|
|
||||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningSocket;
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher;
|
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
|
||||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage;
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
|
||||||
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
|
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.nio.channels.OverlappingFileLockException;
|
import java.nio.channels.OverlappingFileLockException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import kotlin.ResultKt;
|
|
||||||
import kotlin.Unit;
|
|
||||||
import kotlin.coroutines.Continuation;
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext;
|
|
||||||
import kotlin.coroutines.intrinsics.IntrinsicsKt;
|
|
||||||
import kotlin.jvm.functions.Function3;
|
|
||||||
import kotlinx.coroutines.BuildersKt;
|
|
||||||
import kotlinx.coroutines.CoroutineScope;
|
|
||||||
|
|
||||||
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
||||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
|
||||||
|
|
||||||
public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
public class ProvisioningManagerImpl implements ProvisioningManager {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
|
private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
|
||||||
|
|
||||||
@ -79,10 +56,9 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
private final Consumer<Manager> newManagerListener;
|
private final Consumer<Manager> newManagerListener;
|
||||||
private final AccountsStore accountsStore;
|
private final AccountsStore accountsStore;
|
||||||
|
|
||||||
|
private final ProvisioningApi provisioningApi;
|
||||||
|
private final IdentityKeyPair tempIdentityKey;
|
||||||
private final String password;
|
private final String password;
|
||||||
private final CompletableFuture<String> urlFuture = new CompletableFuture<>();
|
|
||||||
private final CompletableFuture<SecondaryProvisioningCipher.ProvisioningDecryptResult<ProvisionMessage>> messageFuture = new CompletableFuture<>();
|
|
||||||
private final Closeable socketHandle;
|
|
||||||
|
|
||||||
public ProvisioningManagerImpl(
|
public ProvisioningManagerImpl(
|
||||||
PathConfig pathConfig,
|
PathConfig pathConfig,
|
||||||
@ -97,58 +73,35 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
this.newManagerListener = newManagerListener;
|
this.newManagerListener = newManagerListener;
|
||||||
this.accountsStore = accountsStore;
|
this.accountsStore = accountsStore;
|
||||||
|
|
||||||
final IdentityKeyPair tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||||
password = KeyUtils.createPassword();
|
password = KeyUtils.createPassword();
|
||||||
|
final var credentialsProvider = new DynamicCredentialsProvider(null,
|
||||||
socketHandle = ProvisioningSocket.Companion.start(new ProvisioningSocket.Mode.Link(false),
|
null,
|
||||||
tempIdentityKey,
|
null,
|
||||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
password,
|
||||||
(id, t) -> {
|
SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||||
urlFuture.completeExceptionally(t);
|
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
messageFuture.completeExceptionally(t);
|
credentialsProvider,
|
||||||
},
|
userAgent,
|
||||||
new ProvisioningBlock());
|
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||||
|
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
|
userAgent);
|
||||||
|
this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
||||||
try {
|
var deviceUuid = provisioningApi.getNewDeviceUuid();
|
||||||
var url = urlFuture.get(30, TimeUnit.SECONDS);
|
|
||||||
return new URI(url);
|
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
||||||
} catch (java.util.concurrent.TimeoutException e) {
|
|
||||||
throw new TimeoutException("Timed out waiting for provisioning URL");
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new IOException("Interrupted while waiting for provisioning URL", e);
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
throw new IOException("Failed to get provisioning URL", e.getCause());
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new IOException("Invalid provisioning URL", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
||||||
SecondaryProvisioningCipher.ProvisioningDecryptResult<ProvisionMessage> decryptResult;
|
var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
|
||||||
try {
|
var number = ret.getNumber();
|
||||||
decryptResult = messageFuture.get(120, TimeUnit.SECONDS);
|
var aci = ret.getAci();
|
||||||
} catch (java.util.concurrent.TimeoutException e) {
|
var pni = ret.getPni();
|
||||||
throw new TimeoutException("Timed out waiting for provisioning message");
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new IOException("Interrupted while waiting for provisioning message", e);
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
throw new IOException("Failed to receive provisioning message", e.getCause());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(decryptResult instanceof SecondaryProvisioningCipher.ProvisioningDecryptResult.Success<ProvisionMessage> success)) {
|
|
||||||
throw new IOException("Failed to decrypt provisioning message");
|
|
||||||
}
|
|
||||||
var msg = success.getMessage();
|
|
||||||
|
|
||||||
var number = msg.number;
|
|
||||||
var aci = ACI.parseOrThrow(msg.aci, msg.aciBinary);
|
|
||||||
var pni = PNI.parseOrThrow(msg.pni, msg.pniBinary);
|
|
||||||
|
|
||||||
logger.info("Received link information from {}, linking in progress ...", number);
|
logger.info("Received link information from {}, linking in progress ...", number);
|
||||||
|
|
||||||
@ -167,42 +120,21 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
accountsStore.updateAccount(accountPath, number, aci);
|
accountsStore.updateAccount(accountPath, number, aci);
|
||||||
}
|
}
|
||||||
|
|
||||||
final IdentityKeyPair aciIdentity;
|
|
||||||
final IdentityKeyPair pniIdentity;
|
|
||||||
final ProfileKey profileKey;
|
|
||||||
try {
|
|
||||||
aciIdentity = new IdentityKeyPair(new IdentityKey(msg.aciIdentityKeyPublic.toByteArray()),
|
|
||||||
new ECPrivateKey(msg.aciIdentityKeyPrivate.toByteArray()));
|
|
||||||
pniIdentity = new IdentityKeyPair(new IdentityKey(msg.pniIdentityKeyPublic.toByteArray()),
|
|
||||||
new ECPrivateKey(msg.pniIdentityKeyPrivate.toByteArray()));
|
|
||||||
profileKey = msg.profileKey == null
|
|
||||||
? KeyUtils.createProfileKey()
|
|
||||||
: new ProfileKey(msg.profileKey.toByteArray());
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IOException("Invalid key material in provisioning message", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
var encryptedDeviceName = deviceName == null
|
var encryptedDeviceName = deviceName == null
|
||||||
? null
|
? null
|
||||||
: DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), aciIdentity);
|
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getAciIdentity().getPrivateKey());
|
||||||
var accountEntropyPool = msg.accountEntropyPool == null ? null : new AccountEntropyPool(msg.accountEntropyPool);
|
// Create new account with the synced identity
|
||||||
var mediaRootBackupKey = msg.mediaRootBackupKey == null
|
var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
|
||||||
? null
|
|
||||||
: new MediaRootBackupKey(msg.mediaRootBackupKey.toByteArray());
|
|
||||||
|
|
||||||
SignalAccount account = null;
|
SignalAccount account = null;
|
||||||
var cleanUpPartialAccountOnFailure = false;
|
|
||||||
var linkingFinished = false;
|
|
||||||
try {
|
try {
|
||||||
if (!accountExists) {
|
if (!accountExists) {
|
||||||
account = SignalAccount.createLinkedAccount(pathConfig.dataPath(),
|
account = SignalAccount.createLinkedAccount(pathConfig.dataPath(),
|
||||||
accountPath,
|
accountPath,
|
||||||
serviceEnvironmentConfig.type(),
|
serviceEnvironmentConfig.type(),
|
||||||
Settings.DEFAULT);
|
Settings.DEFAULT);
|
||||||
cleanUpPartialAccountOnFailure = true;
|
|
||||||
} else {
|
} else {
|
||||||
account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, Settings.DEFAULT);
|
account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, Settings.DEFAULT);
|
||||||
cleanUpPartialAccountOnFailure = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
account.setProvisioningData(number,
|
account.setProvisioningData(number,
|
||||||
@ -210,43 +142,24 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
pni,
|
pni,
|
||||||
password,
|
password,
|
||||||
encryptedDeviceName,
|
encryptedDeviceName,
|
||||||
aciIdentity,
|
ret.getAciIdentity(),
|
||||||
pniIdentity,
|
ret.getPniIdentity(),
|
||||||
profileKey,
|
profileKey,
|
||||||
accountEntropyPool,
|
ret.getAccountEntropyPool(),
|
||||||
msg.authCredentialSalt == null ? null : msg.authCredentialSalt.toByteArray(),
|
ret.getMediaRootBackupKey());
|
||||||
mediaRootBackupKey);
|
|
||||||
|
|
||||||
if (msg.readReceipts != null) {
|
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
|
||||||
account.getConfigurationStore().setReadReceipts(msg.readReceipts);
|
|
||||||
}
|
|
||||||
|
|
||||||
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||||
|
|
||||||
logger.debug("Finishing new device registration");
|
logger.debug("Finishing new device registration");
|
||||||
final var attrs = account.getAccountAttributes(null);
|
var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
||||||
final var deviceAttributes = new DeviceAttributes(attrs.getFetchesMessages(),
|
account.getAccountAttributes(null),
|
||||||
attrs.getRegistrationId(),
|
aciPreKeys,
|
||||||
attrs.getPniRegistrationId(),
|
pniPreKeys);
|
||||||
attrs.getName(),
|
|
||||||
attrs.getCapabilities());
|
|
||||||
final var unauthAccountManager = SignalServiceAccountManager.createWithStaticCredentials(
|
|
||||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
number,
|
|
||||||
SignalServiceAddress.DEFAULT_DEVICE_ID,
|
|
||||||
password,
|
|
||||||
userAgent,
|
|
||||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY,
|
|
||||||
ServiceConfig.GROUP_MAX_SIZE);
|
|
||||||
final var registerResponse = handleResponseException(unauthAccountManager.getRegistrationApi()
|
|
||||||
.registerAsSecondaryDevice(msg.provisioningCode, deviceAttributes, aciPreKeys, pniPreKeys, null));
|
|
||||||
final var deviceId = Integer.parseInt(registerResponse.getDeviceId());
|
|
||||||
|
|
||||||
account.finishLinking(deviceId, aciPreKeys, pniPreKeys);
|
account.finishLinking(deviceId, aciPreKeys, pniPreKeys);
|
||||||
linkingFinished = true;
|
|
||||||
|
|
||||||
ManagerImpl m = null;
|
ManagerImpl m = null;
|
||||||
try {
|
try {
|
||||||
@ -283,12 +196,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
m.close();
|
m.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
if (!linkingFinished && cleanUpPartialAccountOnFailure && account != null) {
|
|
||||||
cleanupPartialAccount(account, accountPath, e);
|
|
||||||
account = null;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
account.close();
|
account.close();
|
||||||
@ -296,33 +203,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
socketHandle.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanupPartialAccount(final SignalAccount account, final String accountPath, final Exception cause) {
|
|
||||||
logger.warn("Link attempt failed before registration completed, removing partial account state for {}.",
|
|
||||||
accountPath,
|
|
||||||
cause);
|
|
||||||
try {
|
|
||||||
account.deleteAccountData();
|
|
||||||
} catch (IOException cleanupError) {
|
|
||||||
logger.warn("Failed to delete partial account data for {}: {}",
|
|
||||||
accountPath,
|
|
||||||
cleanupError.getMessage(),
|
|
||||||
cleanupError);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
accountsStore.removeAccount(accountPath);
|
|
||||||
} catch (RuntimeException cleanupError) {
|
|
||||||
logger.warn("Failed to remove partial account entry for {}: {}",
|
|
||||||
accountPath,
|
|
||||||
cleanupError.getMessage(),
|
|
||||||
cleanupError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
|
private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
|
||||||
final SignalAccount signalAccount;
|
final SignalAccount signalAccount;
|
||||||
try {
|
try {
|
||||||
@ -336,10 +216,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try (signalAccount) {
|
try (signalAccount) {
|
||||||
if (signalAccount.getDeviceId() <= 0) {
|
|
||||||
logger.debug("Account has invalid deviceId {}, allowing relink.", signalAccount.getDeviceId());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (signalAccount.isPrimaryDevice()) {
|
if (signalAccount.isPrimaryDevice()) {
|
||||||
logger.debug("Account is a primary device.");
|
logger.debug("Account is a primary device.");
|
||||||
return false;
|
return false;
|
||||||
@ -367,36 +243,4 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ProvisioningBlock implements Function3<CoroutineScope, ProvisioningSocket<ProvisionMessage>, Continuation<? super Unit>, Object> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object invoke(
|
|
||||||
CoroutineScope scope,
|
|
||||||
ProvisioningSocket<ProvisionMessage> socket,
|
|
||||||
Continuation<? super Unit> cont
|
|
||||||
) {
|
|
||||||
Thread.ofVirtual().start(() -> {
|
|
||||||
try {
|
|
||||||
urlFuture.complete(BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
|
||||||
(s, c) -> socket.getProvisioningUrl(c)));
|
|
||||||
messageFuture.complete(BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
|
||||||
(s, c) -> socket.getProvisioningMessageDecryptResult(c)));
|
|
||||||
cont.resumeWith(Unit.INSTANCE);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
failBoth(new RuntimeException(e), cont);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
failBoth(t, cont);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return IntrinsicsKt.getCOROUTINE_SUSPENDED();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void failBoth(Throwable t, Continuation<? super Unit> cont) {
|
|
||||||
urlFuture.completeExceptionally(t);
|
|
||||||
messageFuture.completeExceptionally(t);
|
|
||||||
cont.resumeWith(ResultKt.createFailure(t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package org.asamk.signal.manager.internal;
|
|||||||
import org.asamk.signal.manager.config.ServiceConfig;
|
import org.asamk.signal.manager.config.ServiceConfig;
|
||||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.signal.core.util.UptimeSleepTimer;
|
|
||||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||||
@ -41,6 +40,7 @@ import org.whispersystems.signalservice.api.services.ProfileService;
|
|||||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
|
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
|
||||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
||||||
@ -355,7 +355,6 @@ public class SignalDependencies {
|
|||||||
() -> preKeyRepository = new PreKeyRepository(getKeysApi(),
|
() -> preKeyRepository = new PreKeyRepository(getKeysApi(),
|
||||||
dataStore.aci(),
|
dataStore.aci(),
|
||||||
localProtocolAddress,
|
localProtocolAddress,
|
||||||
getSessionLock(),
|
|
||||||
Runnable::run));
|
Runnable::run));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
package org.asamk.signal.manager.internal;
|
package org.asamk.signal.manager.internal;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.signal.core.util.SleepTimer;
|
|
||||||
import org.signal.network.util.Preconditions;
|
import org.signal.network.util.Preconditions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||||
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
|
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
|
||||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -104,18 +103,6 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
|||||||
logger.info("Received alerts: {}", String.join(", ", strings));
|
logger.info("Received alerts: {}", String.join(", ", strings));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServerTimestamp(final long serverTimestamp, final boolean isIdentifiedWebsocket) {
|
|
||||||
final var skew = skewFrom(serverTimestamp);
|
|
||||||
if (skew.compareTo(Duration.ofDays(1)) > 0) {
|
|
||||||
logger.warn("Local clock is off from the server by {}, which exceeds the allowed limit..", skew);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Duration skewFrom(long serverTime) {
|
|
||||||
return Duration.ofMillis(Math.abs(System.currentTimeMillis() - serverTime));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
|
* Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
|
||||||
* the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
|
* the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
|
||||||
|
|||||||
@ -9,15 +9,9 @@ public class DownloadProfileJob implements Job {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DownloadProfileJob.class);
|
private static final Logger logger = LoggerFactory.getLogger(DownloadProfileJob.class);
|
||||||
private final RecipientAddress address;
|
private final RecipientAddress address;
|
||||||
private final boolean resolveIdentityKeyConflict;
|
|
||||||
|
|
||||||
public DownloadProfileJob(RecipientAddress address) {
|
public DownloadProfileJob(RecipientAddress address) {
|
||||||
this(address, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DownloadProfileJob(RecipientAddress address, boolean resolveIdentityKeyConflict) {
|
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.resolveIdentityKeyConflict = resolveIdentityKeyConflict;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -25,9 +19,6 @@ public class DownloadProfileJob implements Job {
|
|||||||
logger.trace("Refreshing profile for {}", address);
|
logger.trace("Refreshing profile for {}", address);
|
||||||
final var account = context.getAccount();
|
final var account = context.getAccount();
|
||||||
final var recipientId = account.getRecipientStore().resolveRecipient(address);
|
final var recipientId = account.getRecipientStore().resolveRecipient(address);
|
||||||
final var refreshed = context.getProfileHelper().refreshRecipientProfile(recipientId);
|
context.getProfileHelper().refreshRecipientProfile(recipientId);
|
||||||
if (refreshed && resolveIdentityKeyConflict) {
|
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import java.util.UUID;
|
|||||||
public class AccountDatabase extends Database {
|
public class AccountDatabase extends Database {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||||
private static final long DATABASE_VERSION = 31;
|
private static final long DATABASE_VERSION = 28;
|
||||||
|
|
||||||
private AccountDatabase(final HikariDataSource dataSource) {
|
private AccountDatabase(final HikariDataSource dataSource) {
|
||||||
super(logger, DATABASE_VERSION, dataSource);
|
super(logger, DATABASE_VERSION, dataSource);
|
||||||
@ -623,36 +623,6 @@ public class AccountDatabase extends Database {
|
|||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 29) {
|
|
||||||
logger.debug("Updating database: Adding sticker storage sync columns");
|
|
||||||
try (final var statement = connection.createStatement()) {
|
|
||||||
statement.executeUpdate("""
|
|
||||||
ALTER TABLE sticker ADD COLUMN position INTEGER NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE sticker ADD COLUMN deleted_timestamp INTEGER NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE sticker ADD COLUMN storage_id BLOB;
|
|
||||||
ALTER TABLE sticker ADD COLUMN storage_record BLOB;
|
|
||||||
CREATE UNIQUE INDEX sticker_storage_id_index ON sticker (storage_id);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 30) {
|
|
||||||
logger.debug("Updating database: Create pni_signature_verified column");
|
|
||||||
try (final var statement = connection.createStatement()) {
|
|
||||||
statement.executeUpdate("""
|
|
||||||
ALTER TABLE recipient ADD pni_signature_verified INTEGER NOT NULL DEFAULT FALSE;
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 31) {
|
|
||||||
logger.debug("Updating database: Add blocked-at timestamps");
|
|
||||||
try (final var statement = connection.createStatement()) {
|
|
||||||
statement.executeUpdate("""
|
|
||||||
ALTER TABLE recipient ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE group_v1 ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE group_v2 ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createUuidMappingTable(
|
private static void createUuidMappingTable(
|
||||||
|
|||||||
@ -116,7 +116,7 @@ public class SignalAccount implements Closeable {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||||
|
|
||||||
private static final int MINIMUM_STORAGE_VERSION = 1;
|
private static final int MINIMUM_STORAGE_VERSION = 1;
|
||||||
private static final int CURRENT_STORAGE_VERSION = 11;
|
private static final int CURRENT_STORAGE_VERSION = 10;
|
||||||
|
|
||||||
private final Object LOCK = new Object();
|
private final Object LOCK = new Object();
|
||||||
|
|
||||||
@ -141,7 +141,6 @@ public class SignalAccount implements Closeable {
|
|||||||
private MasterKey pinMasterKey;
|
private MasterKey pinMasterKey;
|
||||||
private StorageKey storageKey;
|
private StorageKey storageKey;
|
||||||
private AccountEntropyPool accountEntropyPool;
|
private AccountEntropyPool accountEntropyPool;
|
||||||
private byte[] authCredentialSalt;
|
|
||||||
private MediaRootBackupKey mediaRootBackupKey;
|
private MediaRootBackupKey mediaRootBackupKey;
|
||||||
private ProfileKey profileKey;
|
private ProfileKey profileKey;
|
||||||
|
|
||||||
@ -154,10 +153,6 @@ public class SignalAccount implements Closeable {
|
|||||||
private final KeyValueEntry<Long> lastReceiveTimestamp = new KeyValueEntry<>("last-receive-timestamp",
|
private final KeyValueEntry<Long> lastReceiveTimestamp = new KeyValueEntry<>("last-receive-timestamp",
|
||||||
long.class,
|
long.class,
|
||||||
0L);
|
0L);
|
||||||
private final KeyValueEntry<Long> lastAppliedPniChangeServerTimestamp = new KeyValueEntry<>(
|
|
||||||
"last-applied-pni-change-server-timestamp",
|
|
||||||
long.class,
|
|
||||||
0L);
|
|
||||||
private final KeyValueEntry<Boolean> needsToRetryFailedMessages = new KeyValueEntry<>("retry-failed-messages",
|
private final KeyValueEntry<Boolean> needsToRetryFailedMessages = new KeyValueEntry<>("retry-failed-messages",
|
||||||
Boolean.class,
|
Boolean.class,
|
||||||
true);
|
true);
|
||||||
@ -297,12 +292,11 @@ public class SignalAccount implements Closeable {
|
|||||||
final ACI aci,
|
final ACI aci,
|
||||||
final PNI pni,
|
final PNI pni,
|
||||||
final String password,
|
final String password,
|
||||||
final byte[] encryptedDeviceName,
|
final String encryptedDeviceName,
|
||||||
final IdentityKeyPair aciIdentity,
|
final IdentityKeyPair aciIdentity,
|
||||||
final IdentityKeyPair pniIdentity,
|
final IdentityKeyPair pniIdentity,
|
||||||
final ProfileKey profileKey,
|
final ProfileKey profileKey,
|
||||||
final AccountEntropyPool accountEntropyPool,
|
final AccountEntropyPool accountEntropyPool,
|
||||||
final byte[] authCredentialSalt,
|
|
||||||
final MediaRootBackupKey mediaRootBackupKey
|
final MediaRootBackupKey mediaRootBackupKey
|
||||||
) {
|
) {
|
||||||
this.deviceId = 0;
|
this.deviceId = 0;
|
||||||
@ -313,13 +307,12 @@ public class SignalAccount implements Closeable {
|
|||||||
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.profileKey = profileKey;
|
this.profileKey = profileKey;
|
||||||
this.encryptedDeviceName = org.signal.core.util.Base64.encodeWithoutPadding(encryptedDeviceName);
|
this.encryptedDeviceName = encryptedDeviceName;
|
||||||
this.aciAccountData.setIdentityKeyPair(aciIdentity);
|
this.aciAccountData.setIdentityKeyPair(aciIdentity);
|
||||||
this.pniAccountData.setIdentityKeyPair(pniIdentity);
|
this.pniAccountData.setIdentityKeyPair(pniIdentity);
|
||||||
this.registered = false;
|
this.registered = false;
|
||||||
this.isMultiDevice = true;
|
this.isMultiDevice = true;
|
||||||
setLastReceiveTimestamp(0L);
|
setLastReceiveTimestamp(0L);
|
||||||
setLastAppliedPniChangeServerTimestamp(0L);
|
|
||||||
if (accountEntropyPool != null) {
|
if (accountEntropyPool != null) {
|
||||||
this.pinMasterKey = null;
|
this.pinMasterKey = null;
|
||||||
this.accountEntropyPool = accountEntropyPool;
|
this.accountEntropyPool = accountEntropyPool;
|
||||||
@ -327,7 +320,6 @@ public class SignalAccount implements Closeable {
|
|||||||
this.pinMasterKey = null;
|
this.pinMasterKey = null;
|
||||||
this.accountEntropyPool = null;
|
this.accountEntropyPool = null;
|
||||||
}
|
}
|
||||||
this.authCredentialSalt = authCredentialSalt;
|
|
||||||
this.mediaRootBackupKey = mediaRootBackupKey;
|
this.mediaRootBackupKey = mediaRootBackupKey;
|
||||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||||
this.setStorageManifest(null);
|
this.setStorageManifest(null);
|
||||||
@ -364,7 +356,6 @@ public class SignalAccount implements Closeable {
|
|||||||
) {
|
) {
|
||||||
this.pinMasterKey = masterKey;
|
this.pinMasterKey = masterKey;
|
||||||
this.accountEntropyPool = null;
|
this.accountEntropyPool = null;
|
||||||
this.authCredentialSalt = null;
|
|
||||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||||
this.setStorageManifest(null);
|
this.setStorageManifest(null);
|
||||||
this.storageKey = null;
|
this.storageKey = null;
|
||||||
@ -377,7 +368,6 @@ public class SignalAccount implements Closeable {
|
|||||||
init();
|
init();
|
||||||
this.registrationLockPin = pin;
|
this.registrationLockPin = pin;
|
||||||
setLastReceiveTimestamp(0L);
|
setLastReceiveTimestamp(0L);
|
||||||
setLastAppliedPniChangeServerTimestamp(0L);
|
|
||||||
save();
|
save();
|
||||||
|
|
||||||
setPreKeys(ServiceIdType.ACI, aciPreKeys);
|
setPreKeys(ServiceIdType.ACI, aciPreKeys);
|
||||||
@ -530,9 +520,6 @@ public class SignalAccount implements Closeable {
|
|||||||
if (storage.accountEntropyPool != null) {
|
if (storage.accountEntropyPool != null) {
|
||||||
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
||||||
}
|
}
|
||||||
if (storage.authCredentialSalt != null) {
|
|
||||||
authCredentialSalt = base64.decode(storage.authCredentialSalt);
|
|
||||||
}
|
|
||||||
if (storage.mediaRootBackupKey != null) {
|
if (storage.mediaRootBackupKey != null) {
|
||||||
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
||||||
}
|
}
|
||||||
@ -913,7 +900,6 @@ public class SignalAccount implements Closeable {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
contact.blocked,
|
contact.blocked,
|
||||||
0,
|
|
||||||
contact.archived,
|
contact.archived,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -1022,7 +1008,6 @@ public class SignalAccount implements Closeable {
|
|||||||
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
||||||
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
||||||
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
||||||
authCredentialSalt == null ? null : base64.encodeToString(authCredentialSalt),
|
|
||||||
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
||||||
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
||||||
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
||||||
@ -1237,11 +1222,6 @@ public class SignalAccount implements Closeable {
|
|||||||
return pniAccountData.getSignalServiceAccountDataStore();
|
return pniAccountData.getSignalServiceAccountDataStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public SignalServiceAccountDataStore pniOrNull() {
|
|
||||||
return getPni() != null ? pniAccountData.getSignalServiceAccountDataStore() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isMultiDevice() {
|
public boolean isMultiDevice() {
|
||||||
return SignalAccount.this.isMultiDevice();
|
return SignalAccount.this.isMultiDevice();
|
||||||
@ -1662,10 +1642,6 @@ public class SignalAccount implements Closeable {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getAuthCredentialSalt() {
|
|
||||||
return authCredentialSalt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRecoveryPassword() {
|
public String getRecoveryPassword() {
|
||||||
final var masterKey = getPinBackedMasterKey();
|
final var masterKey = getPinBackedMasterKey();
|
||||||
if (masterKey == null) {
|
if (masterKey == null) {
|
||||||
@ -1749,7 +1725,7 @@ public class SignalAccount implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRegistered() {
|
public boolean isRegistered() {
|
||||||
return registered && deviceId > 0;
|
return registered;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegistered(final boolean registered) {
|
public void setRegistered(final boolean registered) {
|
||||||
@ -1777,14 +1753,6 @@ public class SignalAccount implements Closeable {
|
|||||||
getKeyValueStore().storeEntry(lastReceiveTimestamp, value);
|
getKeyValueStore().storeEntry(lastReceiveTimestamp, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getLastAppliedPniChangeServerTimestamp() {
|
|
||||||
return getKeyValueStore().getEntry(lastAppliedPniChangeServerTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLastAppliedPniChangeServerTimestamp(final long value) {
|
|
||||||
getKeyValueStore().storeEntry(lastAppliedPniChangeServerTimestamp, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setNeedsToRetryFailedMessages(final boolean value) {
|
public void setNeedsToRetryFailedMessages(final boolean value) {
|
||||||
getKeyValueStore().storeEntry(needsToRetryFailedMessages, value);
|
getKeyValueStore().storeEntry(needsToRetryFailedMessages, value);
|
||||||
}
|
}
|
||||||
@ -1956,8 +1924,7 @@ public class SignalAccount implements Closeable {
|
|||||||
getSessionStore(),
|
getSessionStore(),
|
||||||
getIdentityKeyStore(),
|
getIdentityKeyStore(),
|
||||||
getSenderKeyStore(),
|
getSenderKeyStore(),
|
||||||
SignalAccount.this::isMultiDevice,
|
SignalAccount.this::isMultiDevice));
|
||||||
SignalAccount.this::setMultiDevice));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreKeyStore getPreKeyStore() {
|
public PreKeyStore getPreKeyStore() {
|
||||||
@ -2005,7 +1972,6 @@ public class SignalAccount implements Closeable {
|
|||||||
String pinMasterKey,
|
String pinMasterKey,
|
||||||
String storageKey,
|
String storageKey,
|
||||||
String accountEntropyPool,
|
String accountEntropyPool,
|
||||||
String authCredentialSalt,
|
|
||||||
String mediaRootBackupKey,
|
String mediaRootBackupKey,
|
||||||
String profileKey,
|
String profileKey,
|
||||||
String usernameLinkEntropy,
|
String usernameLinkEntropy,
|
||||||
|
|||||||
@ -58,10 +58,6 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
|
|
||||||
public abstract void setBlocked(boolean blocked);
|
public abstract void setBlocked(boolean blocked);
|
||||||
|
|
||||||
public abstract long getBlockedAt();
|
|
||||||
|
|
||||||
public abstract void setBlockedAt(long blockedAt);
|
|
||||||
|
|
||||||
public abstract boolean isProfileSharingEnabled();
|
public abstract boolean isProfileSharingEnabled();
|
||||||
|
|
||||||
public abstract void setProfileSharingEnabled(boolean profileSharingEnabled);
|
public abstract void setProfileSharingEnabled(boolean profileSharingEnabled);
|
||||||
@ -70,8 +66,6 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
|
|
||||||
public abstract boolean isAnnouncementGroup();
|
public abstract boolean isAnnouncementGroup();
|
||||||
|
|
||||||
public abstract boolean isTerminated();
|
|
||||||
|
|
||||||
public abstract GroupPermission getPermissionAddMember();
|
public abstract GroupPermission getPermissionAddMember();
|
||||||
|
|
||||||
public abstract GroupPermission getPermissionEditDetails();
|
public abstract GroupPermission getPermissionEditDetails();
|
||||||
|
|||||||
@ -24,7 +24,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
public String color;
|
public String color;
|
||||||
public int messageExpirationTime;
|
public int messageExpirationTime;
|
||||||
public boolean blocked;
|
public boolean blocked;
|
||||||
private long blockedAt;
|
|
||||||
public boolean archived;
|
public boolean archived;
|
||||||
private byte[] storageRecord;
|
private byte[] storageRecord;
|
||||||
|
|
||||||
@ -40,7 +39,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
final String color,
|
final String color,
|
||||||
final int messageExpirationTime,
|
final int messageExpirationTime,
|
||||||
final boolean blocked,
|
final boolean blocked,
|
||||||
final long blockedAt,
|
|
||||||
final boolean archived,
|
final boolean archived,
|
||||||
final byte[] storageRecord
|
final byte[] storageRecord
|
||||||
) {
|
) {
|
||||||
@ -51,7 +49,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
this.color = color;
|
this.color = color;
|
||||||
this.messageExpirationTime = messageExpirationTime;
|
this.messageExpirationTime = messageExpirationTime;
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
this.blockedAt = blockedAt;
|
|
||||||
this.archived = archived;
|
this.archived = archived;
|
||||||
this.storageRecord = storageRecord;
|
this.storageRecord = storageRecord;
|
||||||
}
|
}
|
||||||
@ -94,24 +91,9 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBlocked(final boolean blocked) {
|
public void setBlocked(final boolean blocked) {
|
||||||
if (blocked && !this.blocked) {
|
|
||||||
blockedAt = System.currentTimeMillis();
|
|
||||||
} else if (!blocked) {
|
|
||||||
blockedAt = 0;
|
|
||||||
}
|
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBlockedAt() {
|
|
||||||
return blockedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setBlockedAt(final long blockedAt) {
|
|
||||||
this.blockedAt = blockedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isProfileSharingEnabled() {
|
public boolean isProfileSharingEnabled() {
|
||||||
return true;
|
return true;
|
||||||
@ -131,11 +113,6 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isTerminated() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupPermission getPermissionAddMember() {
|
public GroupPermission getPermissionAddMember() {
|
||||||
return GroupPermission.EVERY_MEMBER;
|
return GroupPermission.EVERY_MEMBER;
|
||||||
|
|||||||
@ -23,7 +23,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
private final GroupMasterKey masterKey;
|
private final GroupMasterKey masterKey;
|
||||||
private final DistributionId distributionId;
|
private final DistributionId distributionId;
|
||||||
private boolean blocked;
|
private boolean blocked;
|
||||||
private long blockedAt;
|
|
||||||
private boolean profileSharingEnabled;
|
private boolean profileSharingEnabled;
|
||||||
private DecryptedGroup group;
|
private DecryptedGroup group;
|
||||||
private byte[] storageRecord;
|
private byte[] storageRecord;
|
||||||
@ -48,7 +47,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
final DecryptedGroup group,
|
final DecryptedGroup group,
|
||||||
final DistributionId distributionId,
|
final DistributionId distributionId,
|
||||||
final boolean blocked,
|
final boolean blocked,
|
||||||
final long blockedAt,
|
|
||||||
final boolean profileSharingEnabled,
|
final boolean profileSharingEnabled,
|
||||||
final boolean permissionDenied,
|
final boolean permissionDenied,
|
||||||
final byte[] storageRecord,
|
final byte[] storageRecord,
|
||||||
@ -59,7 +57,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
this.group = group;
|
this.group = group;
|
||||||
this.distributionId = distributionId;
|
this.distributionId = distributionId;
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
this.blockedAt = blockedAt;
|
|
||||||
this.profileSharingEnabled = profileSharingEnabled;
|
this.profileSharingEnabled = profileSharingEnabled;
|
||||||
this.permissionDenied = permissionDenied;
|
this.permissionDenied = permissionDenied;
|
||||||
this.storageRecord = storageRecord;
|
this.storageRecord = storageRecord;
|
||||||
@ -189,24 +186,9 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBlocked(final boolean blocked) {
|
public void setBlocked(final boolean blocked) {
|
||||||
if (blocked && !this.blocked) {
|
|
||||||
blockedAt = System.currentTimeMillis();
|
|
||||||
} else if (!blocked) {
|
|
||||||
blockedAt = 0;
|
|
||||||
}
|
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBlockedAt() {
|
|
||||||
return blockedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setBlockedAt(final long blockedAt) {
|
|
||||||
this.blockedAt = blockedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isProfileSharingEnabled() {
|
public boolean isProfileSharingEnabled() {
|
||||||
return profileSharingEnabled;
|
return profileSharingEnabled;
|
||||||
@ -229,11 +211,6 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
return this.group != null && this.group.isAnnouncementGroup == EnabledState.ENABLED;
|
return this.group != null && this.group.isAnnouncementGroup == EnabledState.ENABLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isTerminated() {
|
|
||||||
return this.group != null && Boolean.TRUE.equals(this.group.terminated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupPermission getPermissionAddMember() {
|
public GroupPermission getPermissionAddMember() {
|
||||||
final var accessControl = getAccessControl();
|
final var accessControl = getAccessControl();
|
||||||
|
|||||||
@ -63,7 +63,6 @@ public class GroupStore {
|
|||||||
distribution_id BLOB UNIQUE NOT NULL,
|
distribution_id BLOB UNIQUE NOT NULL,
|
||||||
endorsement_expiration_time INTEGER NOT NULL DEFAULT 0,
|
endorsement_expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
|
||||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||||
permission_denied INTEGER NOT NULL DEFAULT FALSE
|
permission_denied INTEGER NOT NULL DEFAULT FALSE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
@ -84,7 +83,6 @@ public class GroupStore {
|
|||||||
color TEXT,
|
color TEXT,
|
||||||
expiration_time INTEGER NOT NULL DEFAULT 0,
|
expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
|
||||||
archived INTEGER NOT NULL DEFAULT FALSE
|
archived INTEGER NOT NULL DEFAULT FALSE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
CREATE TABLE group_v1_member (
|
CREATE TABLE group_v1_member (
|
||||||
@ -403,7 +401,6 @@ public class GroupStore {
|
|||||||
deleteGroup(connection, groupInfoV1.getGroupId());
|
deleteGroup(connection, groupInfoV1.getGroupId());
|
||||||
final var groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, recipientResolver);
|
final var groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, recipientResolver);
|
||||||
groupInfoV2.setBlocked(groupInfoV1.isBlocked());
|
groupInfoV2.setBlocked(groupInfoV1.isBlocked());
|
||||||
groupInfoV2.setBlockedAt(groupInfoV1.getBlockedAt());
|
|
||||||
updateGroup(connection, groupInfoV2);
|
updateGroup(connection, groupInfoV2);
|
||||||
logger.debug("Locally migrated group {} to group v2, id: {}",
|
logger.debug("Locally migrated group {} to group v2, id: {}",
|
||||||
groupInfoV1.getGroupId().toBase64(),
|
groupInfoV1.getGroupId().toBase64(),
|
||||||
@ -617,9 +614,9 @@ public class GroupStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
final var sql = """
|
final var sql = """
|
||||||
INSERT INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, blocked_at, archived, storage_id)
|
INSERT INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived, storage_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, group_id_v2=excluded.group_id_v2, name=excluded.name, color=excluded.color, expiration_time=excluded.expiration_time, blocked=excluded.blocked, blocked_at=excluded.blocked_at, archived=excluded.archived, storage_id=excluded.storage_id
|
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, group_id_v2=excluded.group_id_v2, name=excluded.name, color=excluded.color, expiration_time=excluded.expiration_time, blocked=excluded.blocked, archived=excluded.archived, storage_id=excluded.storage_id
|
||||||
RETURNING _id
|
RETURNING _id
|
||||||
""".formatted(TABLE_GROUP_V1);
|
""".formatted(TABLE_GROUP_V1);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
@ -634,9 +631,8 @@ public class GroupStore {
|
|||||||
statement.setString(5, groupV1.color);
|
statement.setString(5, groupV1.color);
|
||||||
statement.setLong(6, groupV1.getMessageExpirationTimer());
|
statement.setLong(6, groupV1.getMessageExpirationTimer());
|
||||||
statement.setBoolean(7, groupV1.isBlocked());
|
statement.setBoolean(7, groupV1.isBlocked());
|
||||||
statement.setLong(8, groupV1.getBlockedAt());
|
statement.setBoolean(8, groupV1.archived);
|
||||||
statement.setBoolean(9, groupV1.archived);
|
statement.setBytes(9, KeyUtils.createRawStorageId());
|
||||||
statement.setBytes(10, KeyUtils.createRawStorageId());
|
|
||||||
final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
||||||
|
|
||||||
if (internalId == null) {
|
if (internalId == null) {
|
||||||
@ -662,9 +658,9 @@ public class GroupStore {
|
|||||||
} else if (group instanceof GroupInfoV2 groupV2) {
|
} else if (group instanceof GroupInfoV2 groupV2) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, blocked_at, permission_denied, storage_id, profile_sharing)
|
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, permission_denied, storage_id, profile_sharing)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, master_key=excluded.master_key, group_data=excluded.group_data, distribution_id=excluded.distribution_id, blocked=excluded.blocked, blocked_at=excluded.blocked_at, permission_denied=excluded.permission_denied, storage_id=excluded.storage_id, profile_sharing=excluded.profile_sharing
|
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, master_key=excluded.master_key, group_data=excluded.group_data, distribution_id=excluded.distribution_id, blocked=excluded.blocked, permission_denied=excluded.permission_denied, storage_id=excluded.storage_id, profile_sharing=excluded.profile_sharing
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V2);
|
).formatted(TABLE_GROUP_V2);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
@ -682,10 +678,9 @@ public class GroupStore {
|
|||||||
}
|
}
|
||||||
statement.setBytes(5, UuidUtil.toByteArray(groupV2.getDistributionId().asUuid()));
|
statement.setBytes(5, UuidUtil.toByteArray(groupV2.getDistributionId().asUuid()));
|
||||||
statement.setBoolean(6, groupV2.isBlocked());
|
statement.setBoolean(6, groupV2.isBlocked());
|
||||||
statement.setLong(7, groupV2.getBlockedAt());
|
statement.setBoolean(7, groupV2.isPermissionDenied());
|
||||||
statement.setBoolean(8, groupV2.isPermissionDenied());
|
statement.setBytes(8, KeyUtils.createRawStorageId());
|
||||||
statement.setBytes(9, KeyUtils.createRawStorageId());
|
statement.setBoolean(9, groupV2.isProfileSharingEnabled());
|
||||||
statement.setBoolean(10, groupV2.isProfileSharingEnabled());
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -696,7 +691,7 @@ public class GroupStore {
|
|||||||
private List<GroupInfoV2> getGroupsV2() {
|
private List<GroupInfoV2> getGroupsV2() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V2);
|
).formatted(TABLE_GROUP_V2);
|
||||||
@ -714,7 +709,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
public GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id = ?
|
WHERE g.group_id = ?
|
||||||
"""
|
"""
|
||||||
@ -748,7 +743,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV2 getGroupV2(Connection connection, StorageId storageId) throws SQLException {
|
public GroupInfoV2 getGroupV2(Connection connection, StorageId storageId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.storage_id = ?
|
WHERE g.storage_id = ?
|
||||||
"""
|
"""
|
||||||
@ -771,7 +766,6 @@ public class GroupStore {
|
|||||||
final var groupData = resultSet.getBytes("group_data");
|
final var groupData = resultSet.getBytes("group_data");
|
||||||
final var distributionId = resultSet.getBytes("distribution_id");
|
final var distributionId = resultSet.getBytes("distribution_id");
|
||||||
final var blocked = resultSet.getBoolean("blocked");
|
final var blocked = resultSet.getBoolean("blocked");
|
||||||
final var blockedAt = resultSet.getLong("blocked_at");
|
|
||||||
final var profileSharingEnabled = resultSet.getBoolean("profile_sharing");
|
final var profileSharingEnabled = resultSet.getBoolean("profile_sharing");
|
||||||
final var permissionDenied = resultSet.getBoolean("permission_denied");
|
final var permissionDenied = resultSet.getBoolean("permission_denied");
|
||||||
final var storageRecord = resultSet.getBytes("storage_record");
|
final var storageRecord = resultSet.getBytes("storage_record");
|
||||||
@ -780,7 +774,6 @@ public class GroupStore {
|
|||||||
groupData == null ? null : DecryptedGroup.ADAPTER.decode(groupData),
|
groupData == null ? null : DecryptedGroup.ADAPTER.decode(groupData),
|
||||||
DistributionId.from(UuidUtil.parseOrThrow(distributionId)),
|
DistributionId.from(UuidUtil.parseOrThrow(distributionId)),
|
||||||
blocked,
|
blocked,
|
||||||
blockedAt,
|
|
||||||
profileSharingEnabled,
|
profileSharingEnabled,
|
||||||
permissionDenied,
|
permissionDenied,
|
||||||
storageRecord,
|
storageRecord,
|
||||||
@ -807,7 +800,7 @@ public class GroupStore {
|
|||||||
private List<GroupInfoV1> getGroupsV1() {
|
private List<GroupInfoV1> getGroupsV1() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1);
|
).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1);
|
||||||
@ -825,7 +818,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException {
|
public GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id = ?
|
WHERE g.group_id = ?
|
||||||
"""
|
"""
|
||||||
@ -859,7 +852,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV1 getGroupV1(Connection connection, StorageId storageId) throws SQLException {
|
public GroupInfoV1 getGroupV1(Connection connection, StorageId storageId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.storage_id = ?
|
WHERE g.storage_id = ?
|
||||||
"""
|
"""
|
||||||
@ -889,7 +882,6 @@ public class GroupStore {
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
final var expirationTime = resultSet.getInt("expiration_time");
|
final var expirationTime = resultSet.getInt("expiration_time");
|
||||||
final var blocked = resultSet.getBoolean("blocked");
|
final var blocked = resultSet.getBoolean("blocked");
|
||||||
final var blockedAt = resultSet.getLong("blocked_at");
|
|
||||||
final var archived = resultSet.getBoolean("archived");
|
final var archived = resultSet.getBoolean("archived");
|
||||||
final var storageRecord = resultSet.getBytes("storage_record");
|
final var storageRecord = resultSet.getBytes("storage_record");
|
||||||
return new GroupInfoV1(GroupId.v1(groupId),
|
return new GroupInfoV1(GroupId.v1(groupId),
|
||||||
@ -899,7 +891,6 @@ public class GroupStore {
|
|||||||
color,
|
color,
|
||||||
expirationTime,
|
expirationTime,
|
||||||
blocked,
|
blocked,
|
||||||
blockedAt,
|
|
||||||
archived,
|
archived,
|
||||||
storageRecord);
|
storageRecord);
|
||||||
}
|
}
|
||||||
@ -911,7 +902,7 @@ public class GroupStore {
|
|||||||
private GroupInfoV1 getGroupV1ByV2Id(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
private GroupInfoV1 getGroupV1ByV2Id(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id_v2 = ?
|
WHERE g.group_id_v2 = ?
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -59,7 +59,6 @@ public class LegacyGroupStore {
|
|||||||
g1.color,
|
g1.color,
|
||||||
g1.messageExpirationTime,
|
g1.messageExpirationTime,
|
||||||
g1.blocked,
|
g1.blocked,
|
||||||
0,
|
|
||||||
g1.archived,
|
g1.archived,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
@ -78,7 +77,6 @@ public class LegacyGroupStore {
|
|||||||
loadDecryptedGroupLocked(groupId, groupCachePath),
|
loadDecryptedGroupLocked(groupId, groupCachePath),
|
||||||
g2.distributionId == null ? DistributionId.create() : DistributionId.from(g2.distributionId),
|
g2.distributionId == null ? DistributionId.create() : DistributionId.from(g2.distributionId),
|
||||||
g2.blocked,
|
g2.blocked,
|
||||||
0,
|
|
||||||
true,
|
true,
|
||||||
g2.permissionDenied,
|
g2.permissionDenied,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@ -26,7 +26,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
||||||
@ -38,7 +37,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||||||
private final IdentityKeyStore identityKeyStore;
|
private final IdentityKeyStore identityKeyStore;
|
||||||
private final SignalServiceSenderKeyStore senderKeyStore;
|
private final SignalServiceSenderKeyStore senderKeyStore;
|
||||||
private final Supplier<Boolean> isMultiDevice;
|
private final Supplier<Boolean> isMultiDevice;
|
||||||
private final Consumer<Boolean> setMultiDeviceCallback;
|
|
||||||
|
|
||||||
public SignalProtocolStore(
|
public SignalProtocolStore(
|
||||||
final SignalServicePreKeyStore preKeyStore,
|
final SignalServicePreKeyStore preKeyStore,
|
||||||
@ -47,8 +45,7 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||||||
final SignalServiceSessionStore sessionStore,
|
final SignalServiceSessionStore sessionStore,
|
||||||
final IdentityKeyStore identityKeyStore,
|
final IdentityKeyStore identityKeyStore,
|
||||||
final SignalServiceSenderKeyStore senderKeyStore,
|
final SignalServiceSenderKeyStore senderKeyStore,
|
||||||
final Supplier<Boolean> isMultiDevice,
|
final Supplier<Boolean> isMultiDevice
|
||||||
final Consumer<Boolean> setMultiDeviceCallback
|
|
||||||
) {
|
) {
|
||||||
this.preKeyStore = preKeyStore;
|
this.preKeyStore = preKeyStore;
|
||||||
this.signedPreKeyStore = signedPreKeyStore;
|
this.signedPreKeyStore = signedPreKeyStore;
|
||||||
@ -57,7 +54,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||||||
this.identityKeyStore = identityKeyStore;
|
this.identityKeyStore = identityKeyStore;
|
||||||
this.senderKeyStore = senderKeyStore;
|
this.senderKeyStore = senderKeyStore;
|
||||||
this.isMultiDevice = isMultiDevice;
|
this.isMultiDevice = isMultiDevice;
|
||||||
this.setMultiDeviceCallback = setMultiDeviceCallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -213,11 +209,6 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||||||
return isMultiDevice.get();
|
return isMultiDevice.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setMultiDevice(final boolean isMultiDevice) {
|
|
||||||
setMultiDeviceCallback.accept(isMultiDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KyberPreKeyRecord loadKyberPreKey(final int kyberPreKeyId) throws InvalidKeyIdException {
|
public KyberPreKeyRecord loadKyberPreKey(final int kyberPreKeyId) throws InvalidKeyIdException {
|
||||||
return kyberPreKeyStore.loadKyberPreKey(kyberPreKeyId);
|
return kyberPreKeyStore.loadKyberPreKey(kyberPreKeyId);
|
||||||
|
|||||||
@ -50,7 +50,6 @@ public class LegacyRecipientStore2 {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
r.contact.blocked,
|
r.contact.blocked,
|
||||||
0,
|
|
||||||
r.contact.archived,
|
r.contact.archived,
|
||||||
r.contact.profileSharingEnabled,
|
r.contact.profileSharingEnabled,
|
||||||
false,
|
false,
|
||||||
@ -101,7 +100,6 @@ public class LegacyRecipientStore2 {
|
|||||||
profile,
|
profile,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
false,
|
|
||||||
null);
|
null);
|
||||||
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
|
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,6 @@ public class Recipient {
|
|||||||
|
|
||||||
private final Long unregisteredTimestamp;
|
private final Long unregisteredTimestamp;
|
||||||
|
|
||||||
private final boolean pniSignatureVerified;
|
|
||||||
|
|
||||||
private final byte[] storageRecord;
|
private final byte[] storageRecord;
|
||||||
|
|
||||||
public Recipient(
|
public Recipient(
|
||||||
@ -38,7 +36,6 @@ public class Recipient {
|
|||||||
final Profile profile,
|
final Profile profile,
|
||||||
final Boolean discoverable,
|
final Boolean discoverable,
|
||||||
final Long unregisteredTimestamp,
|
final Long unregisteredTimestamp,
|
||||||
final boolean pniSignatureVerified,
|
|
||||||
final byte[] storageRecord
|
final byte[] storageRecord
|
||||||
) {
|
) {
|
||||||
this.recipientId = recipientId;
|
this.recipientId = recipientId;
|
||||||
@ -49,7 +46,6 @@ public class Recipient {
|
|||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.discoverable = discoverable;
|
this.discoverable = discoverable;
|
||||||
this.unregisteredTimestamp = unregisteredTimestamp;
|
this.unregisteredTimestamp = unregisteredTimestamp;
|
||||||
this.pniSignatureVerified = pniSignatureVerified;
|
|
||||||
this.storageRecord = storageRecord;
|
this.storageRecord = storageRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +58,6 @@ public class Recipient {
|
|||||||
profile = builder.profile;
|
profile = builder.profile;
|
||||||
discoverable = builder.discoverable;
|
discoverable = builder.discoverable;
|
||||||
unregisteredTimestamp = builder.unregisteredTimestamp;
|
unregisteredTimestamp = builder.unregisteredTimestamp;
|
||||||
pniSignatureVerified = builder.pniSignatureVerified;
|
|
||||||
storageRecord = builder.storageRecord;
|
storageRecord = builder.storageRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,9 +73,6 @@ public class Recipient {
|
|||||||
builder.profileKey = copy.getProfileKey();
|
builder.profileKey = copy.getProfileKey();
|
||||||
builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
|
builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
|
||||||
builder.profile = copy.getProfile();
|
builder.profile = copy.getProfile();
|
||||||
builder.discoverable = copy.getDiscoverable();
|
|
||||||
builder.unregisteredTimestamp = copy.getUnregisteredTimestamp();
|
|
||||||
builder.pniSignatureVerified = copy.isPniSignatureVerified();
|
|
||||||
builder.storageRecord = copy.getStorageRecord();
|
builder.storageRecord = copy.getStorageRecord();
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
@ -121,10 +113,6 @@ public class Recipient {
|
|||||||
return unregisteredTimestamp == null;
|
return unregisteredTimestamp == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPniSignatureVerified() {
|
|
||||||
return pniSignatureVerified;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getStorageRecord() {
|
public byte[] getStorageRecord() {
|
||||||
return storageRecord;
|
return storageRecord;
|
||||||
}
|
}
|
||||||
@ -139,19 +127,12 @@ public class Recipient {
|
|||||||
&& Objects.equals(contact, recipient.contact)
|
&& Objects.equals(contact, recipient.contact)
|
||||||
&& Objects.equals(profileKey, recipient.profileKey)
|
&& Objects.equals(profileKey, recipient.profileKey)
|
||||||
&& Objects.equals(expiringProfileKeyCredential, recipient.expiringProfileKeyCredential)
|
&& Objects.equals(expiringProfileKeyCredential, recipient.expiringProfileKeyCredential)
|
||||||
&& Objects.equals(profile, recipient.profile)
|
&& Objects.equals(profile, recipient.profile);
|
||||||
&& pniSignatureVerified == recipient.pniSignatureVerified;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(recipientId,
|
return Objects.hash(recipientId, address, contact, profileKey, expiringProfileKeyCredential, profile);
|
||||||
address,
|
|
||||||
contact,
|
|
||||||
profileKey,
|
|
||||||
expiringProfileKeyCredential,
|
|
||||||
profile,
|
|
||||||
pniSignatureVerified);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
@ -164,7 +145,6 @@ public class Recipient {
|
|||||||
private Profile profile;
|
private Profile profile;
|
||||||
private Boolean discoverable;
|
private Boolean discoverable;
|
||||||
private Long unregisteredTimestamp;
|
private Long unregisteredTimestamp;
|
||||||
private boolean pniSignatureVerified;
|
|
||||||
private byte[] storageRecord;
|
private byte[] storageRecord;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
@ -210,11 +190,6 @@ public class Recipient {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withPniSignatureVerified(final boolean val) {
|
|
||||||
pniSignatureVerified = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withStorageRecord(final byte[] val) {
|
public Builder withStorageRecord(final byte[] val) {
|
||||||
storageRecord = val;
|
storageRecord = val;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@ -30,7 +30,6 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -52,18 +51,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
|
|
||||||
private final Map<Long, Long> recipientsMerged = new HashMap<>();
|
private final Map<Long, Long> recipientsMerged = new HashMap<>();
|
||||||
|
|
||||||
private static final int MAX_RECIPIENT_CACHE_SIZE = 2000;
|
private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = Collections.synchronizedMap(new LinkedHashMap<>(
|
|
||||||
16,
|
|
||||||
0.75f,
|
|
||||||
true) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean removeEldestEntry(Map.Entry<ServiceId, RecipientWithAddress> eldest) {
|
|
||||||
return size() > MAX_RECIPIENT_CACHE_SIZE;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
public static void createSql(Connection connection) throws SQLException {
|
public static void createSql(Connection connection) throws SQLException {
|
||||||
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
|
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
|
||||||
@ -82,7 +70,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
profile_key BLOB,
|
profile_key BLOB,
|
||||||
profile_key_credential BLOB,
|
profile_key_credential BLOB,
|
||||||
needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
|
needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
|
||||||
pni_signature_verified INTEGER NOT NULL DEFAULT FALSE,
|
|
||||||
|
|
||||||
given_name TEXT,
|
given_name TEXT,
|
||||||
family_name TEXT,
|
family_name TEXT,
|
||||||
@ -96,7 +83,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
expiration_time_version INTEGER DEFAULT 1 NOT NULL,
|
expiration_time_version INTEGER DEFAULT 1 NOT NULL,
|
||||||
mute_until INTEGER NOT NULL DEFAULT 0,
|
mute_until INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
blocked_at INTEGER NOT NULL DEFAULT 0,
|
|
||||||
archived INTEGER NOT NULL DEFAULT FALSE,
|
archived INTEGER NOT NULL DEFAULT FALSE,
|
||||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||||
hide_story INTEGER NOT NULL DEFAULT FALSE,
|
hide_story INTEGER NOT NULL DEFAULT FALSE,
|
||||||
@ -352,7 +338,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
public List<Pair<RecipientId, Contact>> getContacts() {
|
public List<Pair<RecipientId, Contact>> getContacts() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp
|
SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
|
||||||
FROM %s r
|
FROM %s r
|
||||||
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
|
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
|
||||||
"""
|
"""
|
||||||
@ -376,8 +362,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
SELECT r._id,
|
SELECT r._id,
|
||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -397,8 +382,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
SELECT r._id,
|
SELECT r._id,
|
||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -447,8 +431,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
SELECT r._id,
|
SELECT r._id,
|
||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -480,40 +463,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the subset of the given recipients that are currently known to be
|
|
||||||
* unregistered (i.e. have an unregistered timestamp set).
|
|
||||||
* <p>
|
|
||||||
* These can be skipped when sending group messages; otherwise every send
|
|
||||||
* re-attempts them via the slow legacy 1:1 fan-out. The unregistered flag is
|
|
||||||
* maintained independently by profile/CDS discovery and cleared again once a
|
|
||||||
* recipient registers, so they are re-included automatically.
|
|
||||||
*/
|
|
||||||
public Set<RecipientId> getUnregisteredRecipientIds(final Set<RecipientId> recipientIds) {
|
|
||||||
if (recipientIds.isEmpty()) {
|
|
||||||
return Set.of();
|
|
||||||
}
|
|
||||||
final var recipientIdsCommaSeparated = recipientIds.stream()
|
|
||||||
.map(recipientId -> String.valueOf(recipientId.id()))
|
|
||||||
.collect(Collectors.joining(","));
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT r._id
|
|
||||||
FROM %s r
|
|
||||||
WHERE r.unregistered_timestamp IS NOT NULL AND r._id IN (%s)
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_RECIPIENT, recipientIdsCommaSeparated);
|
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
try (var result = Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet)) {
|
|
||||||
return result.collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed read from recipient store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getAllNumbers() {
|
public Set<String> getAllNumbers() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
@ -893,7 +842,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, expiration_time_version = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, blocked_at = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?, hidden = ?
|
SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, expiration_time_version = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?, hidden = ?
|
||||||
WHERE _id = ?
|
WHERE _id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
@ -908,49 +857,25 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
statement.setBoolean(8, contact != null && contact.isProfileSharingEnabled());
|
statement.setBoolean(8, contact != null && contact.isProfileSharingEnabled());
|
||||||
statement.setString(9, contact == null ? null : contact.color());
|
statement.setString(9, contact == null ? null : contact.color());
|
||||||
statement.setBoolean(10, contact != null && contact.isBlocked());
|
statement.setBoolean(10, contact != null && contact.isBlocked());
|
||||||
statement.setLong(11, contact == null ? 0 : contact.blockedAt());
|
statement.setBoolean(11, contact != null && contact.isArchived());
|
||||||
statement.setBoolean(12, contact != null && contact.isArchived());
|
|
||||||
if (contact == null || contact.unregisteredTimestamp() == null) {
|
if (contact == null || contact.unregisteredTimestamp() == null) {
|
||||||
statement.setNull(13, Types.INTEGER);
|
statement.setNull(12, Types.INTEGER);
|
||||||
} else {
|
} else {
|
||||||
statement.setLong(13, contact.unregisteredTimestamp());
|
statement.setLong(12, contact.unregisteredTimestamp());
|
||||||
}
|
}
|
||||||
statement.setString(14, contact == null ? null : contact.nickNameGivenName());
|
statement.setString(13, contact == null ? null : contact.nickNameGivenName());
|
||||||
statement.setString(15, contact == null ? null : contact.nickNameFamilyName());
|
statement.setString(14, contact == null ? null : contact.nickNameFamilyName());
|
||||||
statement.setString(16, contact == null ? null : contact.note());
|
statement.setString(15, contact == null ? null : contact.note());
|
||||||
statement.setBoolean(17, contact != null && contact.isHidden());
|
statement.setBoolean(16, contact != null && contact.isHidden());
|
||||||
statement.setLong(18, recipientId.id());
|
statement.setLong(17, recipientId.id());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
if (contact != null && contact.unregisteredTimestamp() != null) {
|
if (contact != null && contact.unregisteredTimestamp() != null) {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId, contact.unregisteredTimestamp());
|
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
||||||
}
|
}
|
||||||
rotateStorageId(connection, recipientId);
|
rotateStorageId(connection, recipientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void splitForStorageSyncIfNecessary(final Connection connection, final ACI aci) throws SQLException {
|
|
||||||
final var recipient = findByServiceId(connection, aci);
|
|
||||||
if (recipient.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var recipientId = recipient.get().id();
|
|
||||||
final var address = recipient.get().address();
|
|
||||||
if (address.pni().isEmpty() && address.number().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Splitting {} for storage sync", recipientId);
|
|
||||||
final var splitAddress = new RecipientAddress(Optional.empty(),
|
|
||||||
address.pni(),
|
|
||||||
address.number(),
|
|
||||||
Optional.empty());
|
|
||||||
updateRecipientAddress(connection,
|
|
||||||
recipientId,
|
|
||||||
new RecipientAddress(address.aci(), Optional.empty(), Optional.empty(), address.username()));
|
|
||||||
resolveRecipientTrusted(connection, splitAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
|
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final Collection<StorageId> storageIds
|
final Collection<StorageId> storageIds
|
||||||
@ -992,69 +917,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markPniSignatureVerified(final RecipientId recipientId) {
|
|
||||||
logger.debug("Marking {} as pni signature verified", recipientId);
|
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
connection.setAutoCommit(false);
|
|
||||||
if (storePniSignatureVerified(connection, recipientId, true)) {
|
|
||||||
rotateStorageId(connection, recipientId);
|
|
||||||
}
|
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed update recipient store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean storePniSignatureVerified(
|
|
||||||
final Connection connection,
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final boolean value
|
|
||||||
) throws SQLException {
|
|
||||||
return storePniSignatureVerified(connection, recipientId, value, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean storePniSignatureVerified(
|
|
||||||
final Connection connection,
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final boolean value,
|
|
||||||
final boolean force
|
|
||||||
) throws SQLException {
|
|
||||||
if (!force && isPniSignatureVerified(connection, recipientId) == value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET pni_signature_verified = ?
|
|
||||||
WHERE _id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_RECIPIENT);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setBoolean(1, value);
|
|
||||||
statement.setLong(2, recipientId.id());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPniSignatureVerified(
|
|
||||||
final Connection connection,
|
|
||||||
final RecipientId recipientId
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT pni_signature_verified
|
|
||||||
FROM %s
|
|
||||||
WHERE _id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_RECIPIENT);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setLong(1, recipientId.id());
|
|
||||||
return Utils.executeQuerySingleRow(statement, resultSet -> resultSet.getBoolean("pni_signature_verified"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean needsPniSignature(final RecipientId recipientId) {
|
public boolean needsPniSignature(final RecipientId recipientId) {
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
@ -1086,7 +948,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
if (recipientAddress.get().address().aci().isEmpty() || (
|
if (recipientAddress.get().address().aci().isEmpty() || (
|
||||||
contact != null && contact.unregisteredTimestamp() != null
|
contact != null && contact.unregisteredTimestamp() != null
|
||||||
)) {
|
)) {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId, System.currentTimeMillis());
|
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1120,7 +982,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
if (registered) {
|
if (registered) {
|
||||||
markRegistered(connection, recipientId);
|
markRegistered(connection, recipientId);
|
||||||
} else {
|
} else {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId, System.currentTimeMillis());
|
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
||||||
}
|
}
|
||||||
connection.commit();
|
connection.commit();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@ -1130,10 +992,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
|
|
||||||
private void markUnregisteredAndSplitIfNecessary(
|
private void markUnregisteredAndSplitIfNecessary(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final RecipientId recipientId,
|
final RecipientId recipientId
|
||||||
final long unregisteredTimestamp
|
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
markUnregistered(connection, recipientId, unregisteredTimestamp);
|
markUnregistered(connection, recipientId);
|
||||||
final var address = resolveRecipientAddress(connection, recipientId);
|
final var address = resolveRecipientAddress(connection, recipientId);
|
||||||
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
||||||
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
||||||
@ -1180,11 +1041,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markUnregistered(
|
private void markUnregistered(final Connection connection, final RecipientId recipientId) throws SQLException {
|
||||||
final Connection connection,
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final long unregisteredTimestamp
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
@ -1193,7 +1050,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setLong(1, unregisteredTimestamp);
|
statement.setLong(1, System.currentTimeMillis());
|
||||||
statement.setLong(2, recipientId.id());
|
statement.setLong(2, recipientId.id());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
@ -1354,9 +1211,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final List<RecipientId> toBeMergedRecipientIds
|
final List<RecipientId> toBeMergedRecipientIds
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
for (final var toBeMergedRecipientId : toBeMergedRecipientIds) {
|
for (final var toBeMergedRecipientId : toBeMergedRecipientIds) {
|
||||||
if (isPniSignatureVerified(connection, toBeMergedRecipientId)) {
|
|
||||||
storePniSignatureVerified(connection, recipientId, true, true);
|
|
||||||
}
|
|
||||||
recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId);
|
recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId);
|
||||||
deleteRecipient(connection, toBeMergedRecipientId);
|
deleteRecipient(connection, toBeMergedRecipientId);
|
||||||
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId));
|
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId));
|
||||||
@ -1451,12 +1305,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET number = NULL,
|
SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
|
||||||
aci = NULL,
|
|
||||||
pni = NULL,
|
|
||||||
username = NULL,
|
|
||||||
storage_id = NULL,
|
|
||||||
pni_signature_verified = FALSE
|
|
||||||
WHERE _id = ?
|
WHERE _id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
@ -1472,18 +1321,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final RecipientAddress address
|
final RecipientAddress address
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
|
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
|
||||||
final var existingAddress = resolveRecipientAddress(connection, recipientId);
|
|
||||||
final var keepPniSignatureVerified = Objects.equals(existingAddress.aci(), address.aci()) && Objects.equals(
|
|
||||||
existingAddress.pni(),
|
|
||||||
address.pni());
|
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET number = ?,
|
SET number = ?, aci = ?, pni = ?, username = ?
|
||||||
aci = ?,
|
|
||||||
pni = ?,
|
|
||||||
username = ?,
|
|
||||||
pni_signature_verified = ?
|
|
||||||
WHERE _id = ?
|
WHERE _id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
@ -1492,8 +1333,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
statement.setString(2, address.aci().map(ACI::toString).orElse(null));
|
statement.setString(2, address.aci().map(ACI::toString).orElse(null));
|
||||||
statement.setString(3, address.pni().map(PNI::toString).orElse(null));
|
statement.setString(3, address.pni().map(PNI::toString).orElse(null));
|
||||||
statement.setString(4, address.username().orElse(null));
|
statement.setString(4, address.username().orElse(null));
|
||||||
statement.setBoolean(5, keepPniSignatureVerified && isPniSignatureVerified(connection, recipientId));
|
statement.setLong(5, recipientId.id());
|
||||||
statement.setLong(6, recipientId.id());
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
rotateStorageId(connection, recipientId);
|
rotateStorageId(connection, recipientId);
|
||||||
@ -1518,14 +1358,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
RecipientId toBeMergedRecipientId
|
RecipientId toBeMergedRecipientId
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var contact = getContact(connection, recipientId);
|
final var contact = getContact(connection, recipientId);
|
||||||
final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
|
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
|
final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
|
||||||
storeContact(connection, recipientId, toBeMergedContact);
|
storeContact(connection, recipientId, toBeMergedContact);
|
||||||
} else if (toBeMergedContact != null) {
|
|
||||||
final var mergedContact = mergeContacts(contact, toBeMergedContact);
|
|
||||||
if (!contact.equals(mergedContact)) {
|
|
||||||
storeContact(connection, recipientId, mergedContact);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final var profileKey = getProfileKey(connection, recipientId);
|
final var profileKey = getProfileKey(connection, recipientId);
|
||||||
@ -1550,24 +1385,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
|
recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
static Contact mergeContacts(final Contact primary, final Contact secondary) {
|
|
||||||
final var profileSharingEnabled = primary.isProfileSharingEnabled() || secondary.isProfileSharingEnabled();
|
|
||||||
return Contact.newBuilder(primary)
|
|
||||||
.withGivenName(secondary.givenName())
|
|
||||||
.withFamilyName(secondary.familyName())
|
|
||||||
.withMessageExpirationTime(primary.messageExpirationTime() > 0
|
|
||||||
? primary.messageExpirationTime()
|
|
||||||
: secondary.messageExpirationTime())
|
|
||||||
.withMessageExpirationTimeVersion(Math.max(primary.messageExpirationTimeVersion(),
|
|
||||||
secondary.messageExpirationTimeVersion()))
|
|
||||||
.withMuteUntil(primary.muteUntil() > 0 ? primary.muteUntil() : secondary.muteUntil())
|
|
||||||
.withIsBlocked(primary.isBlocked() || secondary.isBlocked())
|
|
||||||
.withBlockedAt(Math.max(primary.blockedAt(), secondary.blockedAt()))
|
|
||||||
.withIsProfileSharingEnabled(profileSharingEnabled)
|
|
||||||
.withIsHidden(profileSharingEnabled ? false : primary.isHidden())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<RecipientWithAddress> findByNumber(
|
private Optional<RecipientWithAddress> findByNumber(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final String number
|
final String number
|
||||||
@ -1647,7 +1464,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
|
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp
|
SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
|
||||||
FROM %s r
|
FROM %s r
|
||||||
WHERE r._id = ? AND (%s)
|
WHERE r._id = ? AND (%s)
|
||||||
"""
|
"""
|
||||||
@ -1734,7 +1551,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
getProfileFromResultSet(resultSet),
|
getProfileFromResultSet(resultSet),
|
||||||
getDiscoverableFromResultSet(resultSet),
|
getDiscoverableFromResultSet(resultSet),
|
||||||
getUnregisteredTimestampFromResultSet(resultSet),
|
getUnregisteredTimestampFromResultSet(resultSet),
|
||||||
resultSet.getBoolean("pni_signature_verified"),
|
|
||||||
getStorageRecordFromResultSet(resultSet));
|
getStorageRecordFromResultSet(resultSet));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1752,7 +1568,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
resultSet.getLong("mute_until"),
|
resultSet.getLong("mute_until"),
|
||||||
resultSet.getBoolean("hide_story"),
|
resultSet.getBoolean("hide_story"),
|
||||||
resultSet.getBoolean("blocked"),
|
resultSet.getBoolean("blocked"),
|
||||||
resultSet.getLong("blocked_at"),
|
|
||||||
resultSet.getBoolean("archived"),
|
resultSet.getBoolean("archived"),
|
||||||
resultSet.getBoolean("profile_sharing"),
|
resultSet.getBoolean("profile_sharing"),
|
||||||
resultSet.getBoolean("hidden"),
|
resultSet.getBoolean("hidden"),
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import java.sql.ResultSet;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -28,15 +28,8 @@ public class SessionStore implements SignalServiceSessionStore {
|
|||||||
|
|
||||||
private static final String TABLE_SESSION = "session";
|
private static final String TABLE_SESSION = "session";
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SessionStore.class);
|
private static final Logger logger = LoggerFactory.getLogger(SessionStore.class);
|
||||||
private static final int MAX_CACHE_SIZE = 1000;
|
|
||||||
|
|
||||||
private final Map<Key, SessionRecord> cachedSessions = new LinkedHashMap<>(16, 0.75f, true) {
|
private final Map<Key, SessionRecord> cachedSessions = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean removeEldestEntry(Map.Entry<Key, SessionRecord> eldest) {
|
|
||||||
return size() > MAX_CACHE_SIZE;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Database database;
|
private final Database database;
|
||||||
private final int accountIdType;
|
private final int accountIdType;
|
||||||
@ -410,7 +403,7 @@ public class SessionStore implements SignalServiceSessionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isActive(SessionRecord record) {
|
private static boolean isActive(SessionRecord record) {
|
||||||
return record != null && record.hasSenderChain();
|
return record != null && record.hasSenderChain(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
record Key(String address, int deviceId) {}
|
record Key(String address, int deviceId) {}
|
||||||
|
|||||||
@ -1,29 +1,10 @@
|
|||||||
package org.asamk.signal.manager.storage.stickers;
|
package org.asamk.signal.manager.storage.stickers;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.StickerPackId;
|
import org.asamk.signal.manager.api.StickerPackId;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
|
||||||
|
|
||||||
public record StickerPack(
|
public record StickerPack(long internalId, StickerPackId packId, byte[] packKey, boolean isInstalled) {
|
||||||
long internalId,
|
|
||||||
StickerPackId packId,
|
|
||||||
byte[] packKey,
|
|
||||||
boolean isInstalled,
|
|
||||||
int position,
|
|
||||||
long deletedTimestamp,
|
|
||||||
StorageId storageId,
|
|
||||||
byte[] storageRecord
|
|
||||||
) {
|
|
||||||
|
|
||||||
public StickerPack(
|
|
||||||
final long internalId,
|
|
||||||
final StickerPackId packId,
|
|
||||||
final byte[] packKey,
|
|
||||||
final boolean isInstalled
|
|
||||||
) {
|
|
||||||
this(internalId, packId, packKey, isInstalled, 0, 0, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StickerPack(final StickerPackId packId, final byte[] packKey) {
|
public StickerPack(final StickerPackId packId, final byte[] packKey) {
|
||||||
this(-1, packId, packKey, false, 0, 0, null, null);
|
this(-1, packId, packKey, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,19 +3,14 @@ package org.asamk.signal.manager.storage.stickers;
|
|||||||
import org.asamk.signal.manager.api.StickerPackId;
|
import org.asamk.signal.manager.api.StickerPackId;
|
||||||
import org.asamk.signal.manager.storage.Database;
|
import org.asamk.signal.manager.storage.Database;
|
||||||
import org.asamk.signal.manager.storage.Utils;
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Types;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class StickerStore {
|
public class StickerStore {
|
||||||
|
|
||||||
@ -32,11 +27,7 @@ public class StickerStore {
|
|||||||
_id INTEGER PRIMARY KEY,
|
_id INTEGER PRIMARY KEY,
|
||||||
pack_id BLOB UNIQUE NOT NULL,
|
pack_id BLOB UNIQUE NOT NULL,
|
||||||
pack_key BLOB NOT NULL,
|
pack_key BLOB NOT NULL,
|
||||||
installed INTEGER NOT NULL DEFAULT FALSE,
|
installed INTEGER NOT NULL DEFAULT FALSE
|
||||||
position INTEGER NOT NULL DEFAULT 0,
|
|
||||||
deleted_timestamp INTEGER NOT NULL DEFAULT 0,
|
|
||||||
storage_id BLOB UNIQUE,
|
|
||||||
storage_record BLOB
|
|
||||||
) STRICT;
|
) STRICT;
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
@ -47,103 +38,55 @@ public class StickerStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<StickerPack> getStickerPacks() {
|
public List<StickerPack> getStickerPacks() {
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
return getStickerPacks(connection);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed read from sticker store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<StickerPack> getStickerPacks(final Connection connection) throws SQLException {
|
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT s._id, s.pack_id, s.pack_key, s.installed, s.position, s.deleted_timestamp, s.storage_id, s.storage_record
|
SELECT s._id, s.pack_id, s.pack_key, s.installed
|
||||||
FROM %s s
|
FROM %s s
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
try (var result = Utils.executeQueryForStream(statement, this::getStickerPackFromResultSet)) {
|
try (var result = Utils.executeQueryForStream(statement, this::getStickerPackFromResultSet)) {
|
||||||
return result.toList();
|
return result.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public StickerPack getStickerPack(StickerPackId packId) {
|
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
return getStickerPack(connection, packId);
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException("Failed read from sticker store", e);
|
throw new RuntimeException("Failed read from sticker store", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public StickerPack getStickerPack(Connection connection, StickerPackId packId) throws SQLException {
|
public StickerPack getStickerPack(StickerPackId packId) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT s._id, s.pack_id, s.pack_key, s.installed, s.position, s.deleted_timestamp, s.storage_id, s.storage_record
|
SELECT s._id, s.pack_id, s.pack_key, s.installed
|
||||||
FROM %s s
|
FROM %s s
|
||||||
WHERE s.pack_id = ?
|
WHERE s.pack_id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setBytes(1, packId.serialize());
|
statement.setBytes(1, packId.serialize());
|
||||||
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
return Utils.executeQueryForOptional(statement, this::getStickerPackFromResultSet).orElse(null);
|
||||||
}
|
}
|
||||||
}
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed read from sticker store", e);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addStickerPack(StickerPack stickerPack) {
|
public void addStickerPack(StickerPack stickerPack) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
INSERT INTO %s (pack_id, pack_key, installed)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
connection.setAutoCommit(false);
|
|
||||||
var storageId = stickerPack.storageId();
|
|
||||||
if (storageId == null && (stickerPack.isInstalled() || stickerPack.deletedTimestamp() > 0)) {
|
|
||||||
storageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
|
||||||
}
|
|
||||||
|
|
||||||
final var position = stickerPack.isInstalled() ? Math.max(stickerPack.position(),
|
|
||||||
getNextPosition(connection)) : 0;
|
|
||||||
var deletedTimestamp = stickerPack.deletedTimestamp();
|
|
||||||
if (!stickerPack.isInstalled() && deletedTimestamp == 0 && storageId != null) {
|
|
||||||
deletedTimestamp = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setBytes(1, stickerPack.packId().serialize());
|
statement.setBytes(1, stickerPack.packId().serialize());
|
||||||
statement.setBytes(2, stickerPack.packKey());
|
statement.setBytes(2, stickerPack.packKey());
|
||||||
statement.setBoolean(3, stickerPack.isInstalled());
|
statement.setBoolean(3, stickerPack.isInstalled());
|
||||||
statement.setInt(4, position);
|
|
||||||
statement.setLong(5, deletedTimestamp);
|
|
||||||
if (storageId == null) {
|
|
||||||
statement.setNull(6, Types.BLOB);
|
|
||||||
} else {
|
|
||||||
statement.setBytes(6, storageId.getRaw());
|
|
||||||
}
|
|
||||||
if (stickerPack.storageRecord() == null) {
|
|
||||||
statement.setNull(7, Types.BLOB);
|
|
||||||
} else {
|
|
||||||
statement.setBytes(7, stickerPack.storageRecord());
|
|
||||||
}
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException("Failed update sticker store", e);
|
throw new RuntimeException("Failed update sticker store", e);
|
||||||
}
|
}
|
||||||
@ -153,279 +96,28 @@ public class StickerStore {
|
|||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?
|
SET installed = ?
|
||||||
WHERE pack_id = ?
|
WHERE pack_id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
connection.setAutoCommit(false);
|
|
||||||
final var existing = getStickerPack(connection, stickerPackId);
|
|
||||||
if (existing == null || existing.isInstalled() == installed) {
|
|
||||||
connection.commit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var newStorageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
|
||||||
final var position = installed ? getNextPosition(connection) : 0;
|
|
||||||
final var deletedTimestamp = installed ? 0 : System.currentTimeMillis();
|
|
||||||
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setBoolean(1, installed);
|
statement.setBytes(1, stickerPackId.serialize());
|
||||||
statement.setInt(2, position);
|
statement.setBoolean(2, installed);
|
||||||
statement.setLong(3, deletedTimestamp);
|
|
||||||
statement.setBytes(4, newStorageId.getRaw());
|
|
||||||
statement.setBytes(5, stickerPackId.serialize());
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException("Failed update sticker store", e);
|
throw new RuntimeException("Failed update sticker store", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<StorageId> getStorageIds(final Connection connection) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT s.storage_id
|
|
||||||
FROM %s s
|
|
||||||
WHERE s.storage_id IS NOT NULL
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
return Utils.executeQueryForStream(statement, this::getStorageIdFromResultSet).toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateStorageId(
|
|
||||||
final Connection connection,
|
|
||||||
final StickerPackId packId,
|
|
||||||
final StorageId storageId
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
statement.setBytes(2, packId.serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateStorageIds(
|
|
||||||
final Connection connection,
|
|
||||||
final Map<StickerPackId, StorageId> storageIdMap
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
for (final var entry : storageIdMap.entrySet()) {
|
|
||||||
statement.setBytes(1, entry.getValue().getRaw());
|
|
||||||
statement.setBytes(2, entry.getKey().serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public StorageId getStorageId(final Connection connection, final StickerPackId packId) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT s.storage_id
|
|
||||||
FROM %s s
|
|
||||||
WHERE s.pack_id = ? AND s.storage_id IS NOT NULL
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
statement.setBytes(1, packId.serialize());
|
|
||||||
final var storageId = Utils.executeQueryForOptional(statement, this::getStorageIdFromResultSet);
|
|
||||||
if (storageId.isPresent()) {
|
|
||||||
return storageId.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final var newStorageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
|
||||||
updateStorageId(connection, packId, newStorageId);
|
|
||||||
return newStorageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void storeStorageRecord(
|
|
||||||
final Connection connection,
|
|
||||||
final StickerPackId packId,
|
|
||||||
final StorageId storageId,
|
|
||||||
final byte[] storageRecord
|
|
||||||
) throws SQLException {
|
|
||||||
final var clearSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = NULL
|
|
||||||
WHERE storage_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(clearSql)) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
final var updateSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = ?, storage_record = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
if (storageRecord == null) {
|
|
||||||
statement.setNull(2, Types.BLOB);
|
|
||||||
} else {
|
|
||||||
statement.setBytes(2, storageRecord);
|
|
||||||
}
|
|
||||||
statement.setBytes(3, packId.serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMissingStorageIds() {
|
|
||||||
final var selectSql = (
|
|
||||||
"""
|
|
||||||
SELECT s.pack_id
|
|
||||||
FROM %s s
|
|
||||||
WHERE s.storage_id IS NULL AND s.installed = TRUE
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
final var updateSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
|
|
||||||
try (final var connection = database.getConnection()) {
|
|
||||||
connection.setAutoCommit(false);
|
|
||||||
try (final var selectStatement = connection.prepareStatement(selectSql)) {
|
|
||||||
final var packIds = Utils.executeQueryForStream(selectStatement,
|
|
||||||
resultSet -> StickerPackId.deserialize(resultSet.getBytes("pack_id"))).toList();
|
|
||||||
try (final var updateStatement = connection.prepareStatement(updateSql)) {
|
|
||||||
for (final var packId : packIds) {
|
|
||||||
updateStatement.setBytes(1, KeyUtils.createRawStorageId());
|
|
||||||
updateStatement.setBytes(2, packId.serialize());
|
|
||||||
updateStatement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connection.commit();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Failed update sticker store", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int removeStorageIdsFromLocalOnlyDeletedStickerPacks(
|
|
||||||
final Connection connection,
|
|
||||||
final Collection<StorageId> storageIds
|
|
||||||
) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET storage_id = NULL
|
|
||||||
WHERE storage_id = ? AND installed = FALSE AND deleted_timestamp > 0
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
var count = 0;
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
for (final var storageId : storageIds) {
|
|
||||||
statement.setBytes(1, storageId.getRaw());
|
|
||||||
count += statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void upsertFromStorageSync(
|
|
||||||
final Connection connection,
|
|
||||||
final SignalStickerPackRecord record
|
|
||||||
) throws SQLException {
|
|
||||||
final var remote = record.getProto();
|
|
||||||
final var packId = StickerPackId.deserialize(remote.packId.toByteArray());
|
|
||||||
final var deleted = remote.deletedAtTimestamp > 0;
|
|
||||||
final var packKey = remote.packKey.toByteArray();
|
|
||||||
final var storageRecord = remote.encode();
|
|
||||||
|
|
||||||
final var current = getStickerPack(connection, packId);
|
|
||||||
|
|
||||||
if (current == null) {
|
|
||||||
final var insertSql = (
|
|
||||||
"""
|
|
||||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(insertSql)) {
|
|
||||||
statement.setBytes(1, packId.serialize());
|
|
||||||
statement.setBytes(2, packKey);
|
|
||||||
statement.setBoolean(3, !deleted);
|
|
||||||
statement.setInt(4, deleted ? 0 : remote.position);
|
|
||||||
statement.setLong(5, remote.deletedAtTimestamp);
|
|
||||||
statement.setBytes(6, record.getId().getRaw());
|
|
||||||
statement.setBytes(7, storageRecord);
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packKey.length > 0) {
|
|
||||||
final var updateSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET pack_key = ?, installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?, storage_record = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
|
||||||
statement.setBytes(1, packKey);
|
|
||||||
statement.setBoolean(2, !deleted);
|
|
||||||
statement.setInt(3, deleted ? 0 : remote.position);
|
|
||||||
statement.setLong(4, remote.deletedAtTimestamp);
|
|
||||||
statement.setBytes(5, record.getId().getRaw());
|
|
||||||
statement.setBytes(6, storageRecord);
|
|
||||||
statement.setBytes(7, packId.serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final var updateSql = (
|
|
||||||
"""
|
|
||||||
UPDATE %s
|
|
||||||
SET installed = ?, position = ?, deleted_timestamp = ?, storage_id = ?, storage_record = ?
|
|
||||||
WHERE pack_id = ?
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(updateSql)) {
|
|
||||||
statement.setBoolean(1, !deleted);
|
|
||||||
statement.setInt(2, deleted ? 0 : remote.position);
|
|
||||||
statement.setLong(3, remote.deletedAtTimestamp);
|
|
||||||
statement.setBytes(4, record.getId().getRaw());
|
|
||||||
statement.setBytes(5, storageRecord);
|
|
||||||
statement.setBytes(6, packId.serialize());
|
|
||||||
statement.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addLegacyStickers(Collection<StickerPack> stickerPacks) {
|
void addLegacyStickers(Collection<StickerPack> stickerPacks) {
|
||||||
logger.debug("Migrating legacy stickers to database");
|
logger.debug("Migrating legacy stickers to database");
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
INSERT INTO %s (pack_id, pack_key, installed, position, deleted_timestamp, storage_id, storage_record)
|
INSERT INTO %s (pack_id, pack_key, installed)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
try (final var connection = database.getConnection()) {
|
try (final var connection = database.getConnection()) {
|
||||||
@ -433,23 +125,11 @@ public class StickerStore {
|
|||||||
try (final var statement = connection.prepareStatement("DELETE FROM %s".formatted(TABLE_STICKER))) {
|
try (final var statement = connection.prepareStatement("DELETE FROM %s".formatted(TABLE_STICKER))) {
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
var installedPosition = 0;
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
for (final var sticker : stickerPacks) {
|
for (final var sticker : stickerPacks) {
|
||||||
final var storageId = sticker.isInstalled()
|
|
||||||
? StorageId.forStickerPack(KeyUtils.createRawStorageId())
|
|
||||||
: null;
|
|
||||||
statement.setBytes(1, sticker.packId().serialize());
|
statement.setBytes(1, sticker.packId().serialize());
|
||||||
statement.setBytes(2, sticker.packKey());
|
statement.setBytes(2, sticker.packKey());
|
||||||
statement.setBoolean(3, sticker.isInstalled());
|
statement.setBoolean(3, sticker.isInstalled());
|
||||||
statement.setInt(4, sticker.isInstalled() ? installedPosition++ : 0);
|
|
||||||
statement.setLong(5, 0);
|
|
||||||
if (storageId == null) {
|
|
||||||
statement.setNull(6, Types.BLOB);
|
|
||||||
} else {
|
|
||||||
statement.setBytes(6, storageId.getRaw());
|
|
||||||
}
|
|
||||||
statement.setNull(7, Types.BLOB);
|
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,36 +145,6 @@ public class StickerStore {
|
|||||||
final var packId = resultSet.getBytes("pack_id");
|
final var packId = resultSet.getBytes("pack_id");
|
||||||
final var packKey = resultSet.getBytes("pack_key");
|
final var packKey = resultSet.getBytes("pack_key");
|
||||||
final var installed = resultSet.getBoolean("installed");
|
final var installed = resultSet.getBoolean("installed");
|
||||||
final var position = resultSet.getInt("position");
|
return new StickerPack(internalId, StickerPackId.deserialize(packId), packKey, installed);
|
||||||
final var deletedTimestamp = resultSet.getLong("deleted_timestamp");
|
|
||||||
final var storageIdBytes = resultSet.getBytes("storage_id");
|
|
||||||
final var storageId = storageIdBytes == null ? null : StorageId.forStickerPack(storageIdBytes);
|
|
||||||
final var storageRecord = resultSet.getBytes("storage_record");
|
|
||||||
return new StickerPack(internalId,
|
|
||||||
StickerPackId.deserialize(packId),
|
|
||||||
packKey,
|
|
||||||
installed,
|
|
||||||
position,
|
|
||||||
deletedTimestamp,
|
|
||||||
storageId,
|
|
||||||
storageRecord);
|
|
||||||
}
|
|
||||||
|
|
||||||
private StorageId getStorageIdFromResultSet(final ResultSet resultSet) throws SQLException {
|
|
||||||
final var storageId = resultSet.getBytes("storage_id");
|
|
||||||
return StorageId.forStickerPack(storageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getNextPosition(final Connection connection) throws SQLException {
|
|
||||||
final var sql = (
|
|
||||||
"""
|
|
||||||
SELECT IFNULL(MAX(position) + 1, 0) AS next_position
|
|
||||||
FROM %s
|
|
||||||
WHERE installed = TRUE
|
|
||||||
"""
|
|
||||||
).formatted(TABLE_STICKER);
|
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
|
||||||
return Utils.executeQuerySingleRow(statement, resultSet -> resultSet.getInt("next_position"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,10 +24,8 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id
|
|||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import okio.ByteString;
|
import okio.ByteString;
|
||||||
@ -48,69 +46,16 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
private final SignalAccount account;
|
private final SignalAccount account;
|
||||||
private final Connection connection;
|
private final Connection connection;
|
||||||
private final JobExecutor jobExecutor;
|
private final JobExecutor jobExecutor;
|
||||||
private final Set<StorageId> identityConflictsPendingRepair;
|
|
||||||
|
|
||||||
public ContactRecordProcessor(
|
public ContactRecordProcessor(SignalAccount account, Connection connection, final JobExecutor jobExecutor) {
|
||||||
SignalAccount account,
|
|
||||||
Connection connection,
|
|
||||||
final JobExecutor jobExecutor,
|
|
||||||
final Set<StorageId> identityConflictsPendingRepair
|
|
||||||
) {
|
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.jobExecutor = jobExecutor;
|
this.jobExecutor = jobExecutor;
|
||||||
this.identityConflictsPendingRepair = identityConflictsPendingRepair;
|
|
||||||
this.selfAci = account.getAci();
|
this.selfAci = account.getAci();
|
||||||
this.selfPni = account.getPni();
|
this.selfPni = account.getPni();
|
||||||
this.selfNumber = account.getNumber();
|
this.selfNumber = account.getNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void prepare(final Collection<SignalContactRecord> remoteRecords) throws SQLException {
|
|
||||||
for (final var remoteRecord : remoteRecords) {
|
|
||||||
if (isInvalid(remoteRecord)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final var remote = remoteRecord.getProto();
|
|
||||||
final var aci = ACI.parseOrNull(remote.aci, remote.aciBinary);
|
|
||||||
final var pni = PNI.parseOrNull(remote.pni, remote.pniBinary);
|
|
||||||
if (shouldSplitForStorageSync(remote.unregisteredAtTimestamp, aci, pni, remote.e164)) {
|
|
||||||
account.getRecipientStore().splitForStorageSyncIfNecessary(connection, aci);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean shouldSplitForStorageSync(
|
|
||||||
final long unregisteredAtTimestamp,
|
|
||||||
final ACI aci,
|
|
||||||
final PNI pni,
|
|
||||||
final String e164
|
|
||||||
) {
|
|
||||||
return unregisteredAtTimestamp > 0 && aci != null && pni == null && e164.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
* Error cases:
|
||||||
* - You can't have a contact record without an ACI or PNI.
|
* - You can't have a contact record without an ACI or PNI.
|
||||||
@ -172,40 +117,32 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
|
|
||||||
IdentityState identityState;
|
IdentityState identityState;
|
||||||
ByteString identityKey;
|
ByteString identityKey;
|
||||||
|
if (remote.identityKey.size() > 0 && (
|
||||||
|
!account.isPrimaryDevice()
|
||||||
|
|| remote.identityState != local.identityState
|
||||||
|
|| local.identityKey.size() == 0
|
||||||
|
|
||||||
|
)) {
|
||||||
|
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 localAci = ACI.parseOrNull(local.aci, local.aciBinary);
|
||||||
final var localPni = PNI.parseOrNull(local.pni, local.pniBinary);
|
final var localPni = PNI.parseOrNull(local.pni, local.pniBinary);
|
||||||
final var remoteAci = ACI.parseOrNull(remote.aci, remote.aciBinary);
|
final var remoteAci = ACI.parseOrNull(remote.aci, remote.aciBinary);
|
||||||
final var remotePni = PNI.parseOrNull(remote.pni, remote.pniBinary);
|
final var remotePni = PNI.parseOrNull(remote.pni, remote.pniBinary);
|
||||||
|
|
||||||
final var hasLocalIdentity = localAci != null || localPni != null;
|
if (localAci != null
|
||||||
final var hasRemoteIdentity = remoteAci != null || remotePni != null;
|
&& local.identityKey.size() > 0
|
||||||
|
&& remote.identityKey.size() > 0
|
||||||
final var remoteIdentityKeySize = remote.identityKey.size();
|
&& !local.identityKey.equals(remote.identityKey)) {
|
||||||
final var localIdentityKeySize = local.identityKey.size();
|
logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
|
||||||
final var statesDiffer = remote.identityState != local.identityState;
|
localAci);
|
||||||
final var identityKeysExistAndConflict = remoteIdentityKeySize > 0
|
final var address = getRecipientAddress(local);
|
||||||
&& localIdentityKeySize > 0
|
jobExecutor.enqueueJob(new DownloadProfileJob(address));
|
||||||
&& !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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PNI pni;
|
PNI pni;
|
||||||
@ -241,17 +178,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
e164 = firstNonEmpty(remote.e164, local.e164);
|
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
|
final var remoteProfileKey = remote.profileKey.size() == 0
|
||||||
|| KeyUtils.profileKeyOrNull(remote.profileKey.toByteArray()) == null
|
|| KeyUtils.profileKeyOrNull(remote.profileKey.toByteArray()) == null
|
||||||
? ByteString.EMPTY
|
? ByteString.EMPTY
|
||||||
@ -268,7 +194,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
.identityState(identityState)
|
.identityState(identityState)
|
||||||
.identityKey(identityKey)
|
.identityKey(identityKey)
|
||||||
.blocked(remote.blocked)
|
.blocked(remote.blocked)
|
||||||
.blockedAtTimestamp(remote.blockedAtTimestamp)
|
|
||||||
.whitelisted(remote.whitelisted)
|
.whitelisted(remote.whitelisted)
|
||||||
.archived(remote.archived)
|
.archived(remote.archived)
|
||||||
.markedUnread(remote.markedUnread)
|
.markedUnread(remote.markedUnread)
|
||||||
@ -276,9 +201,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
.hideStory(remote.hideStory)
|
.hideStory(remote.hideStory)
|
||||||
.unregisteredAtTimestamp(remote.unregisteredAtTimestamp)
|
.unregisteredAtTimestamp(remote.unregisteredAtTimestamp)
|
||||||
.hidden(remote.hidden)
|
.hidden(remote.hidden)
|
||||||
.pniSignatureVerified((remote.pniSignatureVerified || local.pniSignatureVerified)
|
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
|
||||||
&& pni != null
|
|
||||||
&& pni.isValid())
|
|
||||||
.nickname(remote.nickname)
|
.nickname(remote.nickname)
|
||||||
.note(remote.note)
|
.note(remote.note)
|
||||||
.avatarColor(remote.avatarColor);
|
.avatarColor(remote.avatarColor);
|
||||||
@ -313,9 +236,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
|
|
||||||
final var matchesLocal = doProtosMatch(merged, local);
|
final var matchesLocal = doProtosMatch(merged, local);
|
||||||
if (matchesLocal) {
|
if (matchesLocal) {
|
||||||
if (identityKeysExistAndConflict && conflictAci != null) {
|
|
||||||
identityConflictsPendingRepair.add(localRecord.getId());
|
|
||||||
}
|
|
||||||
return localRecord;
|
return localRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,9 +272,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
|
final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
|
||||||
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
|
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
|
||||||
final var contactNote = contact == null ? null : contact.note();
|
final var contactNote = contact == null ? null : contact.note();
|
||||||
final var blockedAt = contact == null ? 0 : contact.blockedAt();
|
|
||||||
if (blocked != contactProto.blocked
|
if (blocked != contactProto.blocked
|
||||||
|| blockedAt != contactProto.blockedAtTimestamp
|
|
||||||
|| profileShared != contactProto.whitelisted
|
|| profileShared != contactProto.whitelisted
|
||||||
|| archived != contactProto.archived
|
|| archived != contactProto.archived
|
||||||
|| hidden != contactProto.hidden
|
|| hidden != contactProto.hidden
|
||||||
@ -372,7 +290,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
logger.debug("Storing new or updated contact {}", recipientId);
|
logger.debug("Storing new or updated contact {}", recipientId);
|
||||||
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
|
final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
|
||||||
.withBlockedAt(contactProto.blocked ? contactProto.blockedAtTimestamp : 0)
|
|
||||||
.withIsProfileSharingEnabled(contactProto.whitelisted)
|
.withIsProfileSharingEnabled(contactProto.whitelisted)
|
||||||
.withIsArchived(contactProto.archived)
|
.withIsArchived(contactProto.archived)
|
||||||
.withIsHidden(contactProto.hidden)
|
.withIsHidden(contactProto.hidden)
|
||||||
@ -429,8 +346,6 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
logger.warn("Received invalid contact identity key from storage");
|
logger.warn("Received invalid contact identity key from storage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
account.getRecipientStore()
|
|
||||||
.storePniSignatureVerified(connection, recipientId, contactProto.pniSignatureVerified);
|
|
||||||
account.getRecipientStore()
|
account.getRecipientStore()
|
||||||
.storeStorageRecord(connection, recipientId, contactRecord.getId(), contactProto.encode());
|
.storeStorageRecord(connection, recipientId, contactRecord.getId(), contactProto.encode());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,15 +84,8 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
|
|||||||
return Collections.unmodifiableSet(updatedStorageIds);
|
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) {
|
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);
|
final var group = account.getGroupStore().getOrCreateGroupV1(connection, groupIdV1);
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
group.setBlocked(groupV1Proto.blocked);
|
group.setBlocked(groupV1Proto.blocked);
|
||||||
group.setBlockedAt(0);
|
|
||||||
account.getGroupStore().updateGroup(connection, group);
|
account.getGroupStore().updateGroup(connection, group);
|
||||||
account.getGroupStore()
|
account.getGroupStore()
|
||||||
.storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
|
.storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
|
||||||
|
|||||||
@ -56,7 +56,6 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||||||
final var mergedBuilder = remote.newBuilder()
|
final var mergedBuilder = remote.newBuilder()
|
||||||
.masterKey(remote.masterKey)
|
.masterKey(remote.masterKey)
|
||||||
.blocked(remote.blocked)
|
.blocked(remote.blocked)
|
||||||
.blockedAtTimestamp(remote.blockedAtTimestamp)
|
|
||||||
.whitelisted(remote.whitelisted)
|
.whitelisted(remote.whitelisted)
|
||||||
.archived(remote.archived)
|
.archived(remote.archived)
|
||||||
.markedUnread(remote.markedUnread)
|
.markedUnread(remote.markedUnread)
|
||||||
@ -94,7 +93,6 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||||||
|
|
||||||
final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
|
final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
|
||||||
group.setBlocked(groupV2Proto.blocked);
|
group.setBlocked(groupV2Proto.blocked);
|
||||||
group.setBlockedAt(groupV2Proto.blocked ? groupV2Proto.blockedAtTimestamp : 0);
|
|
||||||
group.setProfileSharingEnabled(groupV2Proto.whitelisted);
|
group.setProfileSharingEnabled(groupV2Proto.whitelisted);
|
||||||
account.getGroupStore().updateGroup(connection, group);
|
account.getGroupStore().updateGroup(connection, group);
|
||||||
account.getGroupStore()
|
account.getGroupStore()
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
package org.asamk.signal.manager.syncStorage;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.StickerPackId;
|
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class StickerPackRecordProcessor extends DefaultStorageRecordProcessor<SignalStickerPackRecord> {
|
|
||||||
|
|
||||||
private static final int PACK_ID_LENGTH = 16;
|
|
||||||
private static final int PACK_KEY_LENGTH = 32;
|
|
||||||
|
|
||||||
private final SignalAccount account;
|
|
||||||
private final Connection connection;
|
|
||||||
|
|
||||||
public StickerPackRecordProcessor(final SignalAccount account, final Connection connection) {
|
|
||||||
this.account = account;
|
|
||||||
this.connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compare(final SignalStickerPackRecord lhs, final SignalStickerPackRecord rhs) {
|
|
||||||
return lhs.getProto().packId.equals(rhs.getProto().packId) ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isInvalid(final SignalStickerPackRecord remote) {
|
|
||||||
return remote.getProto().packId.size() != PACK_ID_LENGTH || (
|
|
||||||
remote.getProto().deletedAtTimestamp == 0 && remote.getProto().packKey.size() != PACK_KEY_LENGTH
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Optional<SignalStickerPackRecord> getMatching(final SignalStickerPackRecord remote) throws SQLException {
|
|
||||||
final var packId = StickerPackId.deserialize(remote.getProto().packId.toByteArray());
|
|
||||||
final var local = account.getStickerStore().getStickerPack(connection, packId);
|
|
||||||
|
|
||||||
if (local == null || (!local.isInstalled() && local.deletedTimestamp() == 0)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
final StorageId storageId;
|
|
||||||
if (local.storageId() != null) {
|
|
||||||
storageId = local.storageId();
|
|
||||||
} else {
|
|
||||||
storageId = StorageId.forStickerPack(KeyUtils.createRawStorageId());
|
|
||||||
account.getStickerStore().updateStorageId(connection, packId, storageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.of(new SignalStickerPackRecord(storageId, StorageSyncModels.localToRemoteRecord(local)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected SignalStickerPackRecord merge(
|
|
||||||
final SignalStickerPackRecord remoteRecord,
|
|
||||||
final SignalStickerPackRecord localRecord
|
|
||||||
) {
|
|
||||||
final var remote = remoteRecord.getProto();
|
|
||||||
final var local = localRecord.getProto();
|
|
||||||
|
|
||||||
if (shouldKeepLocalDeletion(remote.deletedAtTimestamp, local.deletedAtTimestamp)) {
|
|
||||||
return localRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
return remoteRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean shouldKeepLocalDeletion(final long remoteDeletedAt, final long localDeletedAt) {
|
|
||||||
return remoteDeletedAt > 0 && localDeletedAt > 0 && localDeletedAt < remoteDeletedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void insertLocal(final SignalStickerPackRecord record) throws SQLException {
|
|
||||||
account.getStickerStore().upsertFromStorageSync(connection, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void updateLocal(final StorageRecordUpdate<SignalStickerPackRecord> update) throws SQLException {
|
|
||||||
account.getStickerStore().upsertFromStorageSync(connection, update.newRecord());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,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.groups.GroupInfoV2;
|
||||||
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||||
import org.asamk.signal.manager.storage.recipients.Recipient;
|
import org.asamk.signal.manager.storage.recipients.Recipient;
|
||||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
import org.signal.core.util.UuidUtil;
|
import org.signal.core.util.UuidUtil;
|
||||||
@ -17,14 +16,12 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
|||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord;
|
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink;
|
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.StickerPackRecord;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@ -95,13 +92,9 @@ public final class StorageSyncModels {
|
|||||||
|
|
||||||
public static ContactRecord localToRemoteRecord(Recipient recipient, IdentityInfo identity) {
|
public static ContactRecord localToRemoteRecord(Recipient recipient, IdentityInfo identity) {
|
||||||
final var address = recipient.getAddress();
|
final var address = recipient.getAddress();
|
||||||
final var aciPresent = address.aci().isPresent();
|
|
||||||
|
|
||||||
final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
|
final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
|
||||||
.e164(address.number().orElse(""))
|
.e164(address.number().orElse(""))
|
||||||
.username(address.username().orElse(""))
|
.username(address.username().orElse(""))
|
||||||
.pniSignatureVerified(address.pni().map(PNI::isValid).orElse(false)
|
|
||||||
&& recipient.isPniSignatureVerified())
|
|
||||||
.profileKey(recipient.getProfileKey() == null
|
.profileKey(recipient.getProfileKey() == null
|
||||||
? ByteString.EMPTY
|
? ByteString.EMPTY
|
||||||
: ByteString.of(recipient.getProfileKey().serialize()));
|
: ByteString.of(recipient.getProfileKey().serialize()));
|
||||||
@ -124,7 +117,6 @@ public final class StorageSyncModels {
|
|||||||
.nickname(getNicknameRemoteRecord(recipient.getContact()))
|
.nickname(getNicknameRemoteRecord(recipient.getContact()))
|
||||||
.note(emptyIfNull(recipient.getContact().note()))
|
.note(emptyIfNull(recipient.getContact().note()))
|
||||||
.blocked(recipient.getContact().isBlocked())
|
.blocked(recipient.getContact().isBlocked())
|
||||||
.blockedAtTimestamp(recipient.getContact().blockedAt())
|
|
||||||
.whitelisted(recipient.getContact().isProfileSharingEnabled())
|
.whitelisted(recipient.getContact().isProfileSharingEnabled())
|
||||||
.mutedUntilTimestamp(recipient.getContact().muteUntil())
|
.mutedUntilTimestamp(recipient.getContact().muteUntil())
|
||||||
.hideStory(recipient.getContact().hideStory())
|
.hideStory(recipient.getContact().hideStory())
|
||||||
@ -134,7 +126,7 @@ public final class StorageSyncModels {
|
|||||||
.archived(recipient.getContact().isArchived())
|
.archived(recipient.getContact().isArchived())
|
||||||
.hidden(recipient.getContact().isHidden());
|
.hidden(recipient.getContact().isHidden());
|
||||||
}
|
}
|
||||||
if (identity != null && aciPresent) {
|
if (identity != null) {
|
||||||
builder.identityKey(ByteString.of(identity.getIdentityKey().serialize()))
|
builder.identityKey(ByteString.of(identity.getIdentityKey().serialize()))
|
||||||
.identityState(localToRemote(identity.getTrustLevel()));
|
.identityState(localToRemote(identity.getTrustLevel()));
|
||||||
}
|
}
|
||||||
@ -163,28 +155,10 @@ public final class StorageSyncModels {
|
|||||||
final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
|
final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
|
||||||
builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
|
builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
|
||||||
builder.blocked(group.isBlocked());
|
builder.blocked(group.isBlocked());
|
||||||
builder.blockedAtTimestamp(group.getBlockedAt());
|
|
||||||
builder.whitelisted(group.isProfileSharingEnabled());
|
builder.whitelisted(group.isProfileSharingEnabled());
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StickerPackRecord localToRemoteRecord(StickerPack stickerPack) {
|
|
||||||
final var builder = SignalStickerPackRecord.Companion.newBuilder(stickerPack.storageRecord());
|
|
||||||
builder.packId(ByteString.of(stickerPack.packId().serialize()));
|
|
||||||
|
|
||||||
if (stickerPack.deletedTimestamp() > 0) {
|
|
||||||
builder.packKey(ByteString.EMPTY);
|
|
||||||
builder.position(0);
|
|
||||||
builder.deletedAtTimestamp(stickerPack.deletedTimestamp());
|
|
||||||
} else {
|
|
||||||
builder.packKey(ByteString.of(stickerPack.packKey()));
|
|
||||||
builder.position(stickerPack.position());
|
|
||||||
builder.deletedAtTimestamp(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TrustLevel remoteToLocal(IdentityState identityState) {
|
public static TrustLevel remoteToLocal(IdentityState identityState) {
|
||||||
return switch (identityState) {
|
return switch (identityState) {
|
||||||
case DEFAULT -> TrustLevel.TRUSTED_UNVERIFIED;
|
case DEFAULT -> TrustLevel.TRUSTED_UNVERIFIED;
|
||||||
|
|||||||
@ -168,11 +168,6 @@ public final class StorageSyncValidations {
|
|||||||
throw new DuplicateCallLinkError();
|
throw new DuplicateCallLinkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.STICKER_PACK.getValue());
|
|
||||||
if (ids.size() != new HashSet<>(ids).size()) {
|
|
||||||
throw new DuplicateStickerPackError();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DuplicateRawIdAcrossTypesError();
|
throw new DuplicateRawIdAcrossTypesError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,8 +217,6 @@ public final class StorageSyncValidations {
|
|||||||
|
|
||||||
private static final class DuplicateInsertInWriteError extends Error {}
|
private static final class DuplicateInsertInWriteError extends Error {}
|
||||||
|
|
||||||
private static final class DuplicateStickerPackError extends Error {}
|
|
||||||
|
|
||||||
private static final class InsertNotPresentInFullIdSetError extends Error {}
|
private static final class InsertNotPresentInFullIdSetError extends Error {}
|
||||||
|
|
||||||
private static final class DeletePresentInFullIdSetError extends Error {}
|
private static final class DeletePresentInFullIdSetError extends Error {}
|
||||||
|
|||||||
@ -1,50 +1,28 @@
|
|||||||
package org.asamk.signal.manager.util;
|
package org.asamk.signal.manager.util;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.Message.AttachmentDimensions;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
|
|
||||||
public class AttachmentUtils {
|
public class AttachmentUtils {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AttachmentUtils.class);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
public static SignalServiceAttachmentStream createAttachmentStream(
|
public static SignalServiceAttachmentStream createAttachmentStream(
|
||||||
StreamDetails streamDetails,
|
StreamDetails streamDetails,
|
||||||
Optional<String> name,
|
Optional<String> name,
|
||||||
boolean voiceNote,
|
boolean voiceNote,
|
||||||
AttachmentDimensions dimensions,
|
|
||||||
String blurHash,
|
|
||||||
ResumableUploadSpec resumableUploadSpec
|
ResumableUploadSpec resumableUploadSpec
|
||||||
) throws ResumeLocationInvalidException, IOException {
|
) throws ResumeLocationInvalidException {
|
||||||
final var uploadTimestamp = System.currentTimeMillis();
|
final var uploadTimestamp = System.currentTimeMillis();
|
||||||
final var probedStream = dimensions != null
|
|
||||||
? new ProbedStream(streamDetails.getStream(), dimensions.width(), dimensions.height())
|
|
||||||
: probeImageDimensions(streamDetails);
|
|
||||||
return SignalServiceAttachmentStream.newStreamBuilder()
|
return SignalServiceAttachmentStream.newStreamBuilder()
|
||||||
.withStream(probedStream.inputStream())
|
.withStream(streamDetails.getStream())
|
||||||
.withContentType(streamDetails.getContentType())
|
.withContentType(streamDetails.getContentType())
|
||||||
.withLength(streamDetails.getLength())
|
.withLength(streamDetails.getLength())
|
||||||
.withFileName(name.orElse(null))
|
.withFileName(name.orElse(null))
|
||||||
.withVoiceNote(voiceNote)
|
.withVoiceNote(voiceNote)
|
||||||
.withBlurHash(blurHash)
|
|
||||||
.withWidth(probedStream.width())
|
|
||||||
.withHeight(probedStream.height())
|
|
||||||
.withUploadTimestamp(uploadTimestamp)
|
.withUploadTimestamp(uploadTimestamp)
|
||||||
.withResumableUploadSpec(resumableUploadSpec)
|
.withResumableUploadSpec(resumableUploadSpec)
|
||||||
.withUuid(UUID.randomUUID())
|
.withUuid(UUID.randomUUID())
|
||||||
@ -55,55 +33,7 @@ public class AttachmentUtils {
|
|||||||
StreamDetails streamDetails,
|
StreamDetails streamDetails,
|
||||||
Optional<String> name,
|
Optional<String> name,
|
||||||
ResumableUploadSpec resumableUploadSpec
|
ResumableUploadSpec resumableUploadSpec
|
||||||
) throws ResumeLocationInvalidException, IOException {
|
) throws ResumeLocationInvalidException {
|
||||||
return createAttachmentStream(streamDetails, name, false, null, null, resumableUploadSpec);
|
return createAttachmentStream(streamDetails, name, false, resumableUploadSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the attachment's dimensions if it's an image, so recipients don't get a square-cropped thumbnail.
|
|
||||||
* Falls back to width/height 0 (today's behavior) if the content isn't an image, is too large to probe
|
|
||||||
* cheaply, or fails to parse.
|
|
||||||
*/
|
|
||||||
private static ProbedStream probeImageDimensions(StreamDetails streamDetails) throws IOException {
|
|
||||||
final var contentType = streamDetails.getContentType();
|
|
||||||
final var length = streamDetails.getLength();
|
|
||||||
if (contentType == null
|
|
||||||
|| !contentType.startsWith("image/")
|
|
||||||
|| length <= 0
|
|
||||||
|| length > MAX_DIMENSION_PROBE_SIZE) {
|
|
||||||
return new ProbedStream(streamDetails.getStream(), 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
final var stream = streamDetails.getStream();
|
|
||||||
var width = 0;
|
|
||||||
var height = 0;
|
|
||||||
if (stream instanceof FileInputStream fis) {
|
|
||||||
try {
|
|
||||||
final var image = ImageIO.read(fis);
|
|
||||||
if (image != null) {
|
|
||||||
width = image.getWidth();
|
|
||||||
height = image.getHeight();
|
|
||||||
}
|
|
||||||
fis.getChannel().position(0);
|
|
||||||
} catch (IOException | LinkageError e) {
|
|
||||||
logger.debug("Failed to probe image dimensions, sending without width/height: {}", e.getMessage());
|
|
||||||
fis.getChannel().position(0);
|
|
||||||
}
|
|
||||||
return new ProbedStream(fis, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
final var bytes = stream.readAllBytes();
|
|
||||||
try {
|
|
||||||
final var image = ImageIO.read(new ByteArrayInputStream(bytes));
|
|
||||||
if (image != null) {
|
|
||||||
width = image.getWidth();
|
|
||||||
height = image.getHeight();
|
|
||||||
}
|
|
||||||
} catch (IOException | LinkageError e) {
|
|
||||||
logger.debug("Failed to probe image dimensions, sending without width/height: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
return new ProbedStream(new ByteArrayInputStream(bytes), width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private record ProbedStream(InputStream inputStream, int width, int height) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -78,8 +78,10 @@ public class StickerUtils {
|
|||||||
throw new StickerPackInvalidException("Could not find find " + pack.cover().file());
|
throw new StickerPackInvalidException("Could not find find " + pack.cover().file());
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentType = pack.cover().contentType() != null && !pack.cover().contentType().isEmpty() ? pack.cover()
|
var contentType = pack.cover().contentType() != null && !pack.cover().contentType().isEmpty()
|
||||||
.contentType() : getContentType(rootPath, zip, pack.cover().file());
|
? pack.cover()
|
||||||
|
.contentType()
|
||||||
|
: getContentType(rootPath, zip, pack.cover().file());
|
||||||
cover = new SignalServiceStickerManifestUpload.StickerInfo(data.first(),
|
cover = new SignalServiceStickerManifestUpload.StickerInfo(data.first(),
|
||||||
data.second(),
|
data.second(),
|
||||||
Optional.ofNullable(pack.cover().emoji()).orElse(""),
|
Optional.ofNullable(pack.cover().emoji()).orElse(""),
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.asamk.signal.manager.util;
|
package org.asamk.signal.manager.util;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.BadRequestException;
|
|
||||||
import org.asamk.signal.manager.api.Pair;
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.signal.core.models.ServiceId;
|
import org.signal.core.models.ServiceId;
|
||||||
import org.signal.libsignal.net.BadRequestError;
|
import org.signal.libsignal.net.BadRequestError;
|
||||||
@ -18,6 +17,7 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.Proxy;
|
import java.net.Proxy;
|
||||||
import java.net.ProxySelector;
|
import java.net.ProxySelector;
|
||||||
@ -39,9 +39,6 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
import kotlin.coroutines.Continuation;
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext;
|
|
||||||
import kotlinx.coroutines.BuildersKt;
|
|
||||||
import okio.ByteString;
|
import okio.ByteString;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
@ -56,7 +53,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static StreamDetails createStreamDetailsFromFile(final File file) throws IOException {
|
public static StreamDetails createStreamDetailsFromFile(final File file) throws IOException {
|
||||||
final var stream = new FileInputStream(file);
|
final InputStream stream = new FileInputStream(file);
|
||||||
final var size = file.length();
|
final var size = file.length();
|
||||||
final var mime = MimeUtils.getFileMimeType(file).orElse(MimeUtils.OCTET_STREAM);
|
final var mime = MimeUtils.getFileMimeType(file).orElse(MimeUtils.OCTET_STREAM);
|
||||||
return new StreamDetails(stream, mime, size);
|
return new StreamDetails(stream, mime, size);
|
||||||
@ -164,24 +161,6 @@ public class Utils {
|
|||||||
return NetworkResultUtil.toBasicLegacy(response);
|
return NetworkResultUtil.toBasicLegacy(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T> T runSuspendBlocking(final Function<Continuation<? super T>, Object> call) {
|
|
||||||
try {
|
|
||||||
return (T) BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
|
||||||
(scope, cont) -> call.apply((Continuation<? super T>) cont));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new RuntimeException("Interrupted while waiting for suspend function", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T, E extends BadRequestError> T handleResponseExceptionSuspend(
|
|
||||||
final Function<Continuation<? super RequestResult>, Object> call
|
|
||||||
) throws IOException {
|
|
||||||
return handleResponseException((RequestResult<T, E>) runSuspendBlocking((Function) call));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static <T, E extends BadRequestError> T handleResponseException(final RequestResult<T, E> result) throws IOException {
|
public static <T, E extends BadRequestError> T handleResponseException(final RequestResult<T, E> result) throws IOException {
|
||||||
if (result instanceof RequestResult.Success<?> success) {
|
if (result instanceof RequestResult.Success<?> success) {
|
||||||
@ -195,8 +174,8 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
} else if (result instanceof RequestResult.RetryableNetworkError e) {
|
} else if (result instanceof RequestResult.RetryableNetworkError e) {
|
||||||
throw e.getNetworkError();
|
throw e.getNetworkError();
|
||||||
} else if (result instanceof RequestResult.NonSuccess<?> e) {
|
} else if (result instanceof RequestResult.NonSuccess) {
|
||||||
throw new BadRequestException(e.getError());
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("Unexpected value: " + result);
|
throw new IllegalStateException("Unexpected value: " + result);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,104 +0,0 @@
|
|||||||
package org.asamk.signal.manager.storage.groups;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.Group;
|
|
||||||
import org.asamk.signal.manager.api.GroupId;
|
|
||||||
import org.asamk.signal.manager.groups.GroupUtils;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.TestRecipientId;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.signal.core.models.ServiceId;
|
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
|
||||||
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class GroupInfoTerminatedTest {
|
|
||||||
|
|
||||||
private static final RecipientResolver UNUSED_RESOLVER = new RecipientResolver() {
|
|
||||||
@Override
|
|
||||||
public RecipientId resolveRecipient(final RecipientAddress address) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecipientId resolveRecipient(final long recipientId) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecipientId resolveRecipient(final String identifier) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecipientId resolveRecipient(final ServiceId serviceId) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static GroupMasterKey masterKey() {
|
|
||||||
final var bytes = new byte[32];
|
|
||||||
for (int i = 0; i < bytes.length; i++) {
|
|
||||||
bytes[i] = (byte) (i + 1);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return new GroupMasterKey(bytes);
|
|
||||||
} catch (InvalidInputException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GroupInfoV2 groupV2(final DecryptedGroup group) {
|
|
||||||
final var masterKey = masterKey();
|
|
||||||
return new GroupInfoV2(GroupUtils.getGroupIdV2(masterKey),
|
|
||||||
masterKey,
|
|
||||||
group,
|
|
||||||
DistributionId.create(),
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
UNUSED_RESOLVER);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void v1GroupsAreNeverTerminated() {
|
|
||||||
final var group = new GroupInfoV1(GroupId.v1(new byte[16]));
|
|
||||||
assertFalse(group.isTerminated());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void v2ReadsTerminatedFlagFromDecryptedGroup() {
|
|
||||||
assertTrue(groupV2(new DecryptedGroup.Builder().terminated(true).build()).isTerminated());
|
|
||||||
assertFalse(groupV2(new DecryptedGroup.Builder().terminated(false).build()).isTerminated());
|
|
||||||
assertFalse(groupV2(new DecryptedGroup.Builder().build()).isTerminated());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void v2IsNotTerminatedWhenGroupStateMissing() {
|
|
||||||
final var masterKey = masterKey();
|
|
||||||
final var group = new GroupInfoV2(GroupUtils.getGroupIdV2(masterKey), masterKey, UNUSED_RESOLVER);
|
|
||||||
assertFalse(group.isTerminated());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void groupApiRecordCarriesTerminatedFromModel() {
|
|
||||||
final RecipientId self = TestRecipientId.createTestId(1);
|
|
||||||
|
|
||||||
final var terminated = Group.from(groupV2(new DecryptedGroup.Builder().terminated(true).build()),
|
|
||||||
recipientId -> null,
|
|
||||||
self);
|
|
||||||
assertTrue(terminated.isTerminated());
|
|
||||||
|
|
||||||
final var live = Group.from(groupV2(new DecryptedGroup.Builder().terminated(false).build()),
|
|
||||||
recipientId -> null,
|
|
||||||
self);
|
|
||||||
assertFalse(live.isTerminated());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
package org.asamk.signal.manager.storage.recipients;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.Contact;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class RecipientStoreTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mergeContactsUsesAndroidMergePolicy() {
|
|
||||||
final var primary = Contact.newBuilder()
|
|
||||||
.withGivenName("Primary given")
|
|
||||||
.withFamilyName("Primary family")
|
|
||||||
.withNickName("Primary system nickname")
|
|
||||||
.withNickNameGivenName("Primary nickname given")
|
|
||||||
.withNickNameFamilyName("Primary nickname family")
|
|
||||||
.withNote("Primary note")
|
|
||||||
.withColor("Primary color")
|
|
||||||
.withMessageExpirationTimeVersion(2)
|
|
||||||
.withHideStory(true)
|
|
||||||
.withIsBlocked(false)
|
|
||||||
.withBlockedAt(100)
|
|
||||||
.withIsArchived(true)
|
|
||||||
.withIsHidden(true)
|
|
||||||
.withUnregisteredTimestamp(300L)
|
|
||||||
.build();
|
|
||||||
final var secondary = Contact.newBuilder()
|
|
||||||
.withGivenName("Secondary given")
|
|
||||||
.withFamilyName("Secondary family")
|
|
||||||
.withNickName("Secondary system nickname")
|
|
||||||
.withNickNameGivenName("Secondary nickname given")
|
|
||||||
.withNickNameFamilyName("Secondary nickname family")
|
|
||||||
.withNote("Secondary note")
|
|
||||||
.withColor("Secondary color")
|
|
||||||
.withMessageExpirationTime(60)
|
|
||||||
.withMessageExpirationTimeVersion(3)
|
|
||||||
.withMuteUntil(400)
|
|
||||||
.withIsBlocked(true)
|
|
||||||
.withBlockedAt(200)
|
|
||||||
.withIsProfileSharingEnabled(true)
|
|
||||||
.withIsHidden(true)
|
|
||||||
.withUnregisteredTimestamp(500L)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final var merged = RecipientStore.mergeContacts(primary, secondary);
|
|
||||||
|
|
||||||
assertEquals("Secondary given", merged.givenName());
|
|
||||||
assertEquals("Secondary family", merged.familyName());
|
|
||||||
assertEquals("Primary system nickname", merged.nickName());
|
|
||||||
assertEquals("Primary nickname given", merged.nickNameGivenName());
|
|
||||||
assertEquals("Primary nickname family", merged.nickNameFamilyName());
|
|
||||||
assertEquals("Primary note", merged.note());
|
|
||||||
assertEquals("Primary color", merged.color());
|
|
||||||
assertEquals(60, merged.messageExpirationTime());
|
|
||||||
assertEquals(3, merged.messageExpirationTimeVersion());
|
|
||||||
assertEquals(400, merged.muteUntil());
|
|
||||||
assertTrue(merged.hideStory());
|
|
||||||
assertTrue(merged.isBlocked());
|
|
||||||
assertEquals(200, merged.blockedAt());
|
|
||||||
assertTrue(merged.isArchived());
|
|
||||||
assertTrue(merged.isProfileSharingEnabled());
|
|
||||||
assertFalse(merged.isHidden());
|
|
||||||
assertEquals(300L, merged.unregisteredTimestamp());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mergeContactsPrefersConfiguredPrimaryValues() {
|
|
||||||
final var primary = Contact.newBuilder()
|
|
||||||
.withColor("Primary color")
|
|
||||||
.withMessageExpirationTime(30)
|
|
||||||
.withMessageExpirationTimeVersion(4)
|
|
||||||
.withMuteUntil(100)
|
|
||||||
.withIsHidden(true)
|
|
||||||
.build();
|
|
||||||
final var secondary = Contact.newBuilder()
|
|
||||||
.withColor("Secondary color")
|
|
||||||
.withMessageExpirationTime(60)
|
|
||||||
.withMessageExpirationTimeVersion(3)
|
|
||||||
.withMuteUntil(200)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final var merged = RecipientStore.mergeContacts(primary, secondary);
|
|
||||||
|
|
||||||
assertEquals("Primary color", merged.color());
|
|
||||||
assertEquals(30, merged.messageExpirationTime());
|
|
||||||
assertEquals(4, merged.messageExpirationTimeVersion());
|
|
||||||
assertEquals(100, merged.muteUntil());
|
|
||||||
assertTrue(merged.isHidden());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
package org.asamk.signal.manager.syncStorage;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.signal.core.models.ServiceId.ACI;
|
|
||||||
import org.signal.core.models.ServiceId.PNI;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class StorageRecordProcessorTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void splitsOnlyUnregisteredAciOnlyRecords() {
|
|
||||||
final var aci = ACI.from(UUID.randomUUID());
|
|
||||||
final var pni = PNI.from(UUID.randomUUID());
|
|
||||||
|
|
||||||
assertTrue(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, null, ""));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(0, aci, null, ""));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, null, null, ""));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, pni, ""));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, null, "+12025550123"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void linkedDeviceUsesRemoteIdentityKeyForUnrepairableConflict() {
|
|
||||||
assertTrue(ContactRecordProcessor.shouldUseRemoteIdentityKey(false, false, 33, 33, 0, true));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldUseRemoteIdentityKey(true, false, 33, 33, 0, true));
|
|
||||||
assertFalse(ContactRecordProcessor.shouldUseRemoteIdentityKey(false, false, 33, 33, 0, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void keepsOlderLocalStickerDeletion() {
|
|
||||||
assertTrue(StickerPackRecordProcessor.shouldKeepLocalDeletion(200, 100));
|
|
||||||
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(100, 200));
|
|
||||||
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(200, 0));
|
|
||||||
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(0, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
package org.asamk.signal.manager.syncStorage;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
||||||
|
|
||||||
class StorageSyncLoopDetectorTest {
|
|
||||||
|
|
||||||
private static final long NOW = 1_700_000_000_000L;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void repeatedPayloadIsDeniedAfterThreeCharges() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var write = writeWithInsert(1);
|
|
||||||
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
for (var index = 0; index < 3; index++) {
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.REPEATED_PAYLOAD, 3),
|
|
||||||
detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void storageIdsAreExcludedFromPayloadFingerprint() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
|
|
||||||
assertAllowed(detector.onWriteAttempt(writeWithInsert(1), true, false, NOW));
|
|
||||||
assertAllowed(detector.onWriteAttempt(writeWithInsert(2), true, false, NOW));
|
|
||||||
assertAllowed(detector.onWriteAttempt(writeWithInsert(3), true, false, NOW));
|
|
||||||
assertAllowed(detector.onWriteAttempt(writeWithInsert(4), true, false, NOW));
|
|
||||||
|
|
||||||
assertInstanceOf(StorageSyncLoopDetector.Decision.Denied.class,
|
|
||||||
detector.onWriteAttempt(writeWithInsert(5), true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void convergenceClearsTheContentBucket() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var write = writeWithInsert(1);
|
|
||||||
for (var index = 0; index < 4; index++) {
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
detector.onConverged();
|
|
||||||
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void failedWritesAreRefunded() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var write = writeWithInsert(1);
|
|
||||||
|
|
||||||
for (var index = 0; index < 20; index++) {
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
detector.onWriteFailed(NOW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void failedWriteRefundsOnlyBucketsChargedByLatestAttempt() {
|
|
||||||
final var contentDetector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var repeatedWrite = writeWithInsert(1);
|
|
||||||
for (var index = 0; index < 4; index++) {
|
|
||||||
assertAllowed(contentDetector.onWriteAttempt(repeatedWrite, true, false, NOW));
|
|
||||||
}
|
|
||||||
final var deleteOnlyWrite = new WriteOperationResult(null, List.of(), List.of(new byte[]{1}));
|
|
||||||
assertAllowed(contentDetector.onWriteAttempt(deleteOnlyWrite, true, false, NOW));
|
|
||||||
contentDetector.onWriteFailed(NOW);
|
|
||||||
assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.REPEATED_PAYLOAD, 3),
|
|
||||||
contentDetector.onWriteAttempt(repeatedWrite, true, false, NOW));
|
|
||||||
|
|
||||||
final var rateDetector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
for (var index = 0; index < 100; index++) {
|
|
||||||
assertAllowed(rateDetector.onWriteAttempt(deleteOnlyWrite, true, false, NOW));
|
|
||||||
}
|
|
||||||
assertAllowed(rateDetector.onWriteAttempt(deleteOnlyWrite, false, false, NOW));
|
|
||||||
rateDetector.onWriteFailed(NOW);
|
|
||||||
assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.WRITE_RATE, 100),
|
|
||||||
rateDetector.onWriteAttempt(deleteOnlyWrite, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void rateBucketLimitsDeleteOnlyWrites() {
|
|
||||||
final var detector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var write = new WriteOperationResult(null, List.of(), List.of(new byte[]{1}));
|
|
||||||
|
|
||||||
for (var index = 0; index < 100; index++) {
|
|
||||||
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.WRITE_RATE, 100),
|
|
||||||
detector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void retriesAndSingleDeviceWritesAreExempt() {
|
|
||||||
final var retryDetector = new StorageSyncLoopDetector(() -> true);
|
|
||||||
final var singleDeviceDetector = new StorageSyncLoopDetector(() -> false);
|
|
||||||
final var write = writeWithInsert(1);
|
|
||||||
|
|
||||||
for (var index = 0; index < 20; index++) {
|
|
||||||
assertAllowed(retryDetector.onWriteAttempt(write, true, true, NOW));
|
|
||||||
assertAllowed(singleDeviceDetector.onWriteAttempt(write, true, false, NOW));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static WriteOperationResult writeWithInsert(final int storageIdByte) {
|
|
||||||
final var storageId = StorageId.forType(new byte[]{(byte) storageIdByte}, 99);
|
|
||||||
final var record = new SignalStorageRecord(storageId, new StorageRecord.Builder().build());
|
|
||||||
return new WriteOperationResult(null, List.of(record), List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertAllowed(final StorageSyncLoopDetector.Decision decision) {
|
|
||||||
assertEquals(StorageSyncLoopDetector.Decision.Allowed.INSTANCE, decision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
package org.asamk.signal.manager.util;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.Message.AttachmentDimensions;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
class AttachmentUtilsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createAttachmentStream_setsWidthAndHeightForImage() throws Exception {
|
|
||||||
final var imageBytes = pngBytes(37, 21);
|
|
||||||
final var streamDetails = new StreamDetails(new ByteArrayInputStream(imageBytes),
|
|
||||||
"image/png",
|
|
||||||
imageBytes.length);
|
|
||||||
|
|
||||||
final var attachment = AttachmentUtils.createAttachmentStream(streamDetails, Optional.of("meme.png"), null);
|
|
||||||
|
|
||||||
assertEquals(37, attachment.getWidth());
|
|
||||||
assertEquals(21, attachment.getHeight());
|
|
||||||
assertArrayEquals(imageBytes, attachment.getInputStream().readAllBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createAttachmentStream_leavesWidthAndHeightZeroForNonImage() throws Exception {
|
|
||||||
final var bytes = "not an image".getBytes();
|
|
||||||
final var streamDetails = new StreamDetails(new ByteArrayInputStream(bytes),
|
|
||||||
"application/octet-stream",
|
|
||||||
bytes.length);
|
|
||||||
|
|
||||||
final var attachment = AttachmentUtils.createAttachmentStream(streamDetails, Optional.of("file.bin"), null);
|
|
||||||
|
|
||||||
assertEquals(0, attachment.getWidth());
|
|
||||||
assertEquals(0, attachment.getHeight());
|
|
||||||
assertArrayEquals(bytes, attachment.getInputStream().readAllBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createAttachmentStream_setsSuppliedDimensionsAndBlurHash() throws Exception {
|
|
||||||
final var bytes = "opaque video bytes".getBytes();
|
|
||||||
final var blurHash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
|
|
||||||
final var details = new StreamDetails(new ByteArrayInputStream(bytes), "video/mp4", bytes.length);
|
|
||||||
final var attachment = AttachmentUtils.createAttachmentStream(details,
|
|
||||||
Optional.of("clip.mp4"), false, new AttachmentDimensions(1080, 1920), blurHash, null);
|
|
||||||
|
|
||||||
assertEquals(1080, attachment.getWidth());
|
|
||||||
assertEquals(1920, attachment.getHeight());
|
|
||||||
assertEquals(Optional.of(blurHash), attachment.getBlurHash());
|
|
||||||
assertArrayEquals(bytes, attachment.getInputStream().readAllBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createAttachmentStream_skipsProbingWhenDimensionsSupplied() throws Exception {
|
|
||||||
final var imageBytes = pngBytes(37, 21);
|
|
||||||
final var stream = new ByteArrayInputStream(imageBytes);
|
|
||||||
final var details = new StreamDetails(stream, "image/png", imageBytes.length);
|
|
||||||
final var attachment = AttachmentUtils.createAttachmentStream(details,
|
|
||||||
Optional.of("meme.png"), false, new AttachmentDimensions(100, 200), null, null);
|
|
||||||
|
|
||||||
assertEquals(imageBytes.length, stream.available());
|
|
||||||
assertEquals(100, attachment.getWidth());
|
|
||||||
assertEquals(200, attachment.getHeight());
|
|
||||||
assertArrayEquals(imageBytes, attachment.getInputStream().readAllBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] pngBytes(final int width, final int height) throws Exception {
|
|
||||||
final var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
||||||
final var out = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(image, "png", out);
|
|
||||||
return out.toByteArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
package org.asamk.signal.manager.util;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class LeakyBucketTest {
|
|
||||||
|
|
||||||
private static final long NOW = 1_700_000_000_000L;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void dripsLevelsAndCarriesPartialIntervals() {
|
|
||||||
final var state = new TestState();
|
|
||||||
final var bucket = new LeakyBucket(3, Duration.ofHours(1).toMillis(), state);
|
|
||||||
|
|
||||||
bucket.use(NOW);
|
|
||||||
bucket.use(NOW);
|
|
||||||
bucket.use(NOW);
|
|
||||||
|
|
||||||
assertEquals(2, bucket.level(NOW + Duration.ofMinutes(90).toMillis()));
|
|
||||||
assertEquals(1, bucket.level(NOW + Duration.ofMinutes(121).toMillis()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void refundNeverDropsBelowZero() {
|
|
||||||
final var bucket = new LeakyBucket(1, 1_000, new TestState());
|
|
||||||
|
|
||||||
bucket.refund(NOW);
|
|
||||||
|
|
||||||
assertEquals(0, bucket.level(NOW));
|
|
||||||
assertTrue(bucket.hasRoom(NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void clockMovingBackwardsRefillsTheBucket() {
|
|
||||||
final var bucket = new LeakyBucket(1, 1_000, new TestState());
|
|
||||||
bucket.use(NOW);
|
|
||||||
|
|
||||||
assertTrue(bucket.hasRoom(NOW - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class TestState implements LeakyBucket.State {
|
|
||||||
|
|
||||||
private int level;
|
|
||||||
private long levelUpdatedAt;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int level() {
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long levelUpdatedAt() {
|
|
||||||
return levelUpdatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void update(final int level, final long levelUpdatedAt) {
|
|
||||||
this.level = level;
|
|
||||||
this.levelUpdatedAt = levelUpdatedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +1 @@
|
|||||||
0.102.1
|
0.94.1
|
||||||
|
|||||||
@ -452,7 +452,6 @@ Groups have the following (case-sensitive) properties:
|
|||||||
* IsBlocked<b> : true=member will not receive group messages; false=not blocked
|
* IsBlocked<b> : true=member will not receive group messages; false=not blocked
|
||||||
* IsMember<b> (read-only) : always true (object path exists only for group members)
|
* IsMember<b> (read-only) : always true (object path exists only for group members)
|
||||||
* IsAdmin<b> (read-only) : true=member has admin privileges; false=not admin
|
* IsAdmin<b> (read-only) : true=member has admin privileges; false=not admin
|
||||||
* IsTerminated<b> (read-only) : true=group was permanently ended by an admin; nobody (not even an admin) can send messages or start calls, sending is rejected locally
|
|
||||||
* MessageExpirationTimer<i> : int32 representing message expiration time for group
|
* MessageExpirationTimer<i> : int32 representing message expiration time for group
|
||||||
* Members<as> (read-only) : String array of group members' phone numbers
|
* Members<as> (read-only) : String array of group members' phone numbers
|
||||||
* PendingMembers<as> (read-only) : String array of pending members' phone numbers
|
* PendingMembers<as> (read-only) : String array of pending members' phone numbers
|
||||||
@ -503,11 +502,6 @@ Exceptions: Failure
|
|||||||
quitGroup() -> <>::
|
quitGroup() -> <>::
|
||||||
Exceptions: Failure, LastGroupAdmin
|
Exceptions: Failure, LastGroupAdmin
|
||||||
|
|
||||||
terminateGroup() -> <>::
|
|
||||||
Permanently terminate the group for all members (requires admin privileges). Afterwards nobody can send messages or start calls; members keep access to the existing history.
|
|
||||||
|
|
||||||
Exceptions: Failure
|
|
||||||
|
|
||||||
removeAdmins(recipients<as>) -> <>::
|
removeAdmins(recipients<as>) -> <>::
|
||||||
* recipients : String array of phone numbers
|
* recipients : String array of phone numbers
|
||||||
|
|
||||||
|
|||||||
@ -344,15 +344,6 @@ Data URI encoded attachments must follow the RFC 2397.
|
|||||||
Additionally a file name can be added:
|
Additionally a file name can be added:
|
||||||
e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
|
e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
|
||||||
|
|
||||||
*--attachment-dimensions* [WIDTHxHEIGHT [WIDTHxHEIGHT ...]]::
|
|
||||||
Specify the displayed dimensions (after rotation) of the attachment at the same position in `--attachment`.
|
|
||||||
Use an empty string to skip one; image dimensions are otherwise detected automatically.
|
|
||||||
|
|
||||||
*--attachment-blurhash* [BLURHASH [BLURHASH ...]]::
|
|
||||||
Specify a BlurHash (https://blurha.sh) for the attachment at the same position in `--attachment`, which clients show while it's downloading.
|
|
||||||
Use an empty string to skip one.
|
|
||||||
e.g.: `--attachment a.jpg clip.mp4 --attachment-dimensions '' 1080x1920 --attachment-blurhash '' 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'`
|
|
||||||
|
|
||||||
*--view-once*::
|
*--view-once*::
|
||||||
Send the message as a view once message.
|
Send the message as a view once message.
|
||||||
A conformant client will only allow the receiver to view the message once.
|
A conformant client will only allow the receiver to view the message once.
|
||||||
@ -641,20 +632,6 @@ Specify the timestamp of the message to which to react.
|
|||||||
*--type* TYPE::
|
*--type* TYPE::
|
||||||
Specify the receipt type, either `read` (the default) or `viewed`.
|
Specify the receipt type, either `read` (the default) or `viewed`.
|
||||||
|
|
||||||
=== sendStory
|
|
||||||
|
|
||||||
Post a file attachment story to your Story, visible to all contacts.
|
|
||||||
|
|
||||||
*-a* ATTACHMENT, *--attachment* ATTACHMENT::
|
|
||||||
Specify the file path to the image or video to post as a story.
|
|
||||||
|
|
||||||
*-g* GROUP, *--group-id* GROUP::
|
|
||||||
Specify a group to post the story to.
|
|
||||||
Without this flag, the story is posted to "My Story".
|
|
||||||
|
|
||||||
*--no-replies*::
|
|
||||||
Disable replies on this story. By default, replies are allowed.
|
|
||||||
|
|
||||||
=== sendTyping
|
=== sendTyping
|
||||||
|
|
||||||
Send typing message to trigger a typing indicator for the recipient.
|
Send typing message to trigger a typing indicator for the recipient.
|
||||||
@ -795,16 +772,6 @@ Specify the recipient group ID in base64 encoding.
|
|||||||
*--delete*::
|
*--delete*::
|
||||||
Delete local group data completely after quitting group.
|
Delete local group data completely after quitting group.
|
||||||
|
|
||||||
=== terminateGroup
|
|
||||||
|
|
||||||
Permanently terminate a group for all members.
|
|
||||||
This requires admin privileges.
|
|
||||||
Afterwards no member (not even an admin) can send messages or start calls in the group; members keep access to the existing message history.
|
|
||||||
Only supported for Signal group v2 groups.
|
|
||||||
|
|
||||||
*-g* GROUP, *--group-id* GROUP::
|
|
||||||
Specify the group ID in base64 encoding.
|
|
||||||
|
|
||||||
=== listGroups
|
=== listGroups
|
||||||
|
|
||||||
Show a list of known groups and related information.
|
Show a list of known groups and related information.
|
||||||
|
|||||||
@ -18,13 +18,12 @@ fi
|
|||||||
VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1)
|
VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1)
|
||||||
echo "$VERSION" >dist/VERSION
|
echo "$VERSION" >dist/VERSION
|
||||||
|
|
||||||
# Build jar and schemas
|
# Build jar
|
||||||
$ENGINE build -t signal-cli:build ${OVERRIDE_JAVA_VERSION:+--build-arg ZULU_TAG=$OVERRIDE_JAVA_VERSION} -f reproducible-builds/build.Containerfile .
|
$ENGINE build -t signal-cli:build ${OVERRIDE_JAVA_VERSION:+--build-arg ZULU_TAG=$OVERRIDE_JAVA_VERSION} -f reproducible-builds/build.Containerfile .
|
||||||
git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**'
|
git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**'
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:build
|
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:build
|
||||||
mv build/distributions/signal-cli-*.tar.gz dist/
|
mv build/distributions/signal-cli-*.tar.gz dist/
|
||||||
mv build/signal-cli-*-json-schemas.tar.gz dist/
|
|
||||||
|
|
||||||
if [ -n "${OVERRIDE_JAVA_VERSION:-}" ]; then
|
if [ -n "${OVERRIDE_JAVA_VERSION:-}" ]; then
|
||||||
echo -e "\e[33mBuild was performed with overridden Java version $OVERRIDE_JAVA_VERSION, native-image and client will not be built.\e[0m"
|
echo -e "\e[33mBuild was performed with overridden Java version $OVERRIDE_JAVA_VERSION, native-image and client will not be built.\e[0m"
|
||||||
|
|||||||
@ -12,7 +12,7 @@ reset_file_dates
|
|||||||
|
|
||||||
if [ "$1" == "build" ]; then
|
if [ "$1" == "build" ]; then
|
||||||
|
|
||||||
./gradlew build jsonSchemas \
|
./gradlew build \
|
||||||
--no-daemon \
|
--no-daemon \
|
||||||
--max-workers=1 \
|
--max-workers=1 \
|
||||||
-Dkotlin.compiler.execution.strategy=in-process \
|
-Dkotlin.compiler.execution.strategy=in-process \
|
||||||
@ -20,12 +20,6 @@ if [ "$1" == "build" ]; then
|
|||||||
-Dorg.gradle.caching=false \
|
-Dorg.gradle.caching=false \
|
||||||
-Porg.gradle.java.installations.auto-download=false \
|
-Porg.gradle.java.installations.auto-download=false \
|
||||||
-Porg.gradle.java.installations.auto-detect=false
|
-Porg.gradle.java.installations.auto-detect=false
|
||||||
|
|
||||||
schemas_tar="build/signal-cli-${VERSION}-json-schemas.tar"
|
|
||||||
reset_file_dates
|
|
||||||
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner -cf "$schemas_tar" -C build/generated/META-INF/schemas .
|
|
||||||
gzip -n -9 "$schemas_tar"
|
|
||||||
|
|
||||||
cd man
|
cd man
|
||||||
make install
|
make install
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
39
run_tests.sh
39
run_tests.sh
@ -38,13 +38,6 @@ else
|
|||||||
SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli"
|
SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Prefer line-buffered output for external commands when available
|
|
||||||
if command -v stdbuf >/dev/null 2>&1; then
|
|
||||||
STD_BUF="stdbuf -oL -eL --"
|
|
||||||
else
|
|
||||||
STD_BUF=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
run() {
|
run() {
|
||||||
# To update graalvm config, set GRAALVM_HOME, e.g:
|
# To update graalvm config, set GRAALVM_HOME, e.g:
|
||||||
# export GRAALVM_HOME=/usr/lib/jvm/java-25-graalvm
|
# export GRAALVM_HOME=/usr/lib/jvm/java-25-graalvm
|
||||||
@ -55,23 +48,11 @@ run() {
|
|||||||
|
|
||||||
set -x
|
set -x
|
||||||
if [ "$JSON_RPC" -eq 1 ]; then
|
if [ "$JSON_RPC" -eq 1 ]; then
|
||||||
if [ -n "$STD_BUF" ]; then
|
|
||||||
$STD_BUF "$SIGNAL_CLI" $@
|
|
||||||
else
|
|
||||||
"$SIGNAL_CLI" $@
|
"$SIGNAL_CLI" $@
|
||||||
fi
|
|
||||||
elif [ "$DBUS" -eq 1 ]; then
|
elif [ "$DBUS" -eq 1 ]; then
|
||||||
if [ -n "$STD_BUF" ]; then
|
"$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org'
|
||||||
$STD_BUF "$SIGNAL_CLI" --dbus --verbose --verbose $@ > >(grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' || true)
|
|
||||||
else
|
else
|
||||||
"$SIGNAL_CLI" --dbus --verbose --verbose $@ > >(grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' || true)
|
"$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org'
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ -n "$STD_BUF" ]; then
|
|
||||||
$STD_BUF "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ > >(grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' || true)
|
|
||||||
else
|
|
||||||
"$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ > >(grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' || true)
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
set +x
|
set +x
|
||||||
}
|
}
|
||||||
@ -117,10 +98,9 @@ link() {
|
|||||||
rm -f "$LINK_CODE_FILE"
|
rm -f "$LINK_CODE_FILE"
|
||||||
mkfifo "$LINK_CODE_FILE"
|
mkfifo "$LINK_CODE_FILE"
|
||||||
run_linked link -n "test-device" >"$LINK_CODE_FILE" &
|
run_linked link -n "test-device" >"$LINK_CODE_FILE" &
|
||||||
LINK_PID=$!
|
read LINK_CODE <"$LINK_CODE_FILE"
|
||||||
read -r LINK_CODE <"$LINK_CODE_FILE"
|
|
||||||
run_main -a "$NUMBER" addDevice --uri "$LINK_CODE"
|
run_main -a "$NUMBER" addDevice --uri "$LINK_CODE"
|
||||||
wait $LINK_PID
|
wait
|
||||||
run_linked -a "$NUMBER" send --note-to-self -m hi
|
run_linked -a "$NUMBER" send --note-to-self -m hi
|
||||||
run_main -a "$NUMBER" receive
|
run_main -a "$NUMBER" receive
|
||||||
run_linked -a "$NUMBER" receive
|
run_linked -a "$NUMBER" receive
|
||||||
@ -200,7 +180,6 @@ run_main -a "$NUMBER_2" updateContact "$NUMBER_1" -n NUMBER_1 -e 10
|
|||||||
run_main -a "$NUMBER_2" block "$NUMBER_1"
|
run_main -a "$NUMBER_2" block "$NUMBER_1"
|
||||||
run_main -a "$NUMBER_2" unblock "$NUMBER_1"
|
run_main -a "$NUMBER_2" unblock "$NUMBER_1"
|
||||||
run_main -a "$NUMBER_2" listContacts
|
run_main -a "$NUMBER_2" listContacts
|
||||||
run_main -a "$NUMBER_2" listContacts "$NUMBER_1"
|
|
||||||
|
|
||||||
run_main -a "$NUMBER_1" send "$NUMBER_2" -m hi
|
run_main -a "$NUMBER_1" send "$NUMBER_2" -m hi
|
||||||
run_main -a "$NUMBER_2" receive
|
run_main -a "$NUMBER_2" receive
|
||||||
@ -213,7 +192,6 @@ run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
|
|||||||
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hii
|
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hii
|
||||||
run_main -a "$NUMBER_1" updateAccount --discoverable-by-number=false
|
run_main -a "$NUMBER_1" updateAccount --discoverable-by-number=false
|
||||||
run_main -a "$NUMBER_1" receive
|
run_main -a "$NUMBER_1" receive
|
||||||
run_main -a "$NUMBER_1" send "$NUMBER_2" -m hi
|
|
||||||
run_main -a "$NUMBER_2" receive
|
run_main -a "$NUMBER_2" receive
|
||||||
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
|
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
|
||||||
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hii
|
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hii
|
||||||
@ -229,7 +207,6 @@ run_main -a "$NUMBER_1" updateGroup -g "$GROUP_ID" -m "$NUMBER_2"
|
|||||||
run_main -a "$NUMBER_1" listGroups -d
|
run_main -a "$NUMBER_1" listGroups -d
|
||||||
run_main -a "$NUMBER_1" --output=json listGroups -d
|
run_main -a "$NUMBER_1" --output=json listGroups -d
|
||||||
run_main -a "$NUMBER_2" receive
|
run_main -a "$NUMBER_2" receive
|
||||||
run_main -a "$NUMBER_2" listGroups -g "$GROUP_ID"
|
|
||||||
run_main -a "$NUMBER_2" quitGroup -g "$GROUP_ID"
|
run_main -a "$NUMBER_2" quitGroup -g "$GROUP_ID"
|
||||||
run_main -a "$NUMBER_2" listGroups -d
|
run_main -a "$NUMBER_2" listGroups -d
|
||||||
run_main -a "$NUMBER_2" --output=json listGroups -d
|
run_main -a "$NUMBER_2" --output=json listGroups -d
|
||||||
@ -251,7 +228,6 @@ for OUTPUT in "plain-text" "json"; do
|
|||||||
run_main -a "$NUMBER_2" --output="$OUTPUT" receive
|
run_main -a "$NUMBER_2" --output="$OUTPUT" receive
|
||||||
run_main -a "$NUMBER_1" --output="$OUTPUT" receive
|
run_main -a "$NUMBER_1" --output="$OUTPUT" receive
|
||||||
run_main -a "$NUMBER_1" --output="$OUTPUT" send -e "$NUMBER_2"
|
run_main -a "$NUMBER_1" --output="$OUTPUT" send -e "$NUMBER_2"
|
||||||
run_main -a "$NUMBER_1" --output="$OUTPUT" send "$NUMBER_2" -m test
|
|
||||||
run_main -a "$NUMBER_2" --output="$OUTPUT" receive
|
run_main -a "$NUMBER_2" --output="$OUTPUT" receive
|
||||||
done
|
done
|
||||||
|
|
||||||
@ -259,8 +235,8 @@ done
|
|||||||
run_main -a "$NUMBER_1" updateProfile --given-name=GIVEN --family-name=FAMILY --about=ABOUT --about-emoji=EMOJI --avatar=LICENSE --mobile-coin-address="YWJjCg=="
|
run_main -a "$NUMBER_1" updateProfile --given-name=GIVEN --family-name=FAMILY --about=ABOUT --about-emoji=EMOJI --avatar=LICENSE --mobile-coin-address="YWJjCg=="
|
||||||
|
|
||||||
## Provisioning
|
## Provisioning
|
||||||
link "$NUMBER_1" || true
|
link "$NUMBER_1"
|
||||||
link "$NUMBER_2" || true
|
link "$NUMBER_2"
|
||||||
run_main -a "$NUMBER_1" listDevices
|
run_main -a "$NUMBER_1" listDevices
|
||||||
run_linked -a "$NUMBER_1" sendSyncRequest
|
run_linked -a "$NUMBER_1" sendSyncRequest
|
||||||
run_main -a "$NUMBER_1" sendContacts
|
run_main -a "$NUMBER_1" sendContacts
|
||||||
@ -295,7 +271,6 @@ fi
|
|||||||
run_main -a "$NUMBER_2" deleteLocalAccountData || true
|
run_main -a "$NUMBER_2" deleteLocalAccountData || true
|
||||||
|
|
||||||
if [ ! -z "$GRAALVM_HOME" ]; then
|
if [ ! -z "$GRAALVM_HOME" ]; then
|
||||||
NATIVE_IMAGE_UTILS="$GRAALVM_HOME/lib/svm/bin/native-image-utils"
|
"$GRAALVM_HOME"/lib/svm/bin/native-image-configure generate --input-dir=src/main/resources/META-INF/native-image/org.asamk/signal-cli/ --input-dir=graalvm-config-dir-linked/ --input-dir=graalvm-config-dir-main/ --output-dir=src/main/resources/META-INF/native-image/org.asamk/signal-cli/
|
||||||
"$NATIVE_IMAGE_UTILS" generate --input-dir=src/main/resources/META-INF/native-image/org.asamk/signal-cli/ --input-dir=graalvm-config-dir-linked/ --input-dir=graalvm-config-dir-main/ --output-dir=src/main/resources/META-INF/native-image/org.asamk/signal-cli/
|
|
||||||
rm -r graalvm-config-dir-main graalvm-config-dir-linked
|
rm -r graalvm-config-dir-main graalvm-config-dir-linked
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -22,8 +22,6 @@ public interface Signal extends DBusInterface {
|
|||||||
|
|
||||||
String getSelfNumber();
|
String getSelfNumber();
|
||||||
|
|
||||||
String getSelfACI();
|
|
||||||
|
|
||||||
void subscribeReceive();
|
void subscribeReceive();
|
||||||
|
|
||||||
void unsubscribeReceive();
|
void unsubscribeReceive();
|
||||||
@ -595,7 +593,6 @@ public interface Signal extends DBusInterface {
|
|||||||
@DBusProperty(name = "IsBlocked", type = Boolean.class)
|
@DBusProperty(name = "IsBlocked", type = Boolean.class)
|
||||||
@DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ)
|
@DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ)
|
||||||
@DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ)
|
@DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ)
|
||||||
@DBusProperty(name = "IsTerminated", type = Boolean.class, access = DBusProperty.Access.READ)
|
|
||||||
@DBusProperty(name = "MessageExpirationTimer", type = Integer.class)
|
@DBusProperty(name = "MessageExpirationTimer", type = Integer.class)
|
||||||
@DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ)
|
@DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ)
|
||||||
@DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ)
|
@DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ)
|
||||||
@ -612,8 +609,6 @@ public interface Signal extends DBusInterface {
|
|||||||
|
|
||||||
void deleteGroup() throws Error.Failure;
|
void deleteGroup() throws Error.Failure;
|
||||||
|
|
||||||
void terminateGroup() throws Error.Failure;
|
|
||||||
|
|
||||||
void addMembers(List<String> recipients) throws Error.Failure;
|
void addMembers(List<String> recipients) throws Error.Failure;
|
||||||
|
|
||||||
void removeMembers(List<String> recipients) throws Error.Failure;
|
void removeMembers(List<String> recipients) throws Error.Failure;
|
||||||
|
|||||||
@ -60,7 +60,7 @@ public class App {
|
|||||||
parser.addArgument("-v", "--verbose")
|
parser.addArgument("-v", "--verbose")
|
||||||
.help("Raise log level and include lib signal logs. Specify multiple times for even more logs.")
|
.help("Raise log level and include lib signal logs. Specify multiple times for even more logs.")
|
||||||
.action(Arguments.count())
|
.action(Arguments.count())
|
||||||
.setDefault(cfg.verbose() == null ? 0 : cfg.verbose());
|
.setDefault(cfg.verbose());
|
||||||
parser.addArgument("--log-file")
|
parser.addArgument("--log-file")
|
||||||
.type(File.class)
|
.type(File.class)
|
||||||
.help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file.")
|
.help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file.")
|
||||||
@ -68,7 +68,7 @@ public class App {
|
|||||||
parser.addArgument("--scrub-log")
|
parser.addArgument("--scrub-log")
|
||||||
.action(Arguments.storeTrue())
|
.action(Arguments.storeTrue())
|
||||||
.help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.")
|
.help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.")
|
||||||
.setDefault(cfg.scrubLog() == null ? false : cfg.scrubLog());
|
.setDefault(cfg.scrubLog());
|
||||||
parser.addArgument("-d", "--data-dir", "-c", "--config")
|
parser.addArgument("-d", "--data-dir", "-c", "--config")
|
||||||
.help("Set the path where to store data (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).")
|
.help("Set the path where to store data (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).")
|
||||||
.setDefault(cfg.dataDir());
|
.setDefault(cfg.dataDir());
|
||||||
@ -81,12 +81,12 @@ public class App {
|
|||||||
.dest("global-dbus")
|
.dest("global-dbus")
|
||||||
.help("Make request via user dbus.")
|
.help("Make request via user dbus.")
|
||||||
.action(Arguments.storeTrue())
|
.action(Arguments.storeTrue())
|
||||||
.setDefault(cfg.dbus() == null ? false : cfg.dbus());
|
.setDefault(cfg.dbus());
|
||||||
mut.addArgument("--dbus-system")
|
mut.addArgument("--dbus-system")
|
||||||
.dest("global-dbus-system")
|
.dest("global-dbus-system")
|
||||||
.help("Make request via system dbus.")
|
.help("Make request via system dbus.")
|
||||||
.action(Arguments.storeTrue())
|
.action(Arguments.storeTrue())
|
||||||
.setDefault(cfg.dbusSystem() == null ? false : cfg.dbusSystem());
|
.setDefault(cfg.dbusSystem());
|
||||||
parser.addArgument("--bus-name")
|
parser.addArgument("--bus-name")
|
||||||
.dest("global-bus-name")
|
.dest("global-bus-name")
|
||||||
.setDefault(cfg.busName() != null ? cfg.busName() : DbusConfig.getBusname())
|
.setDefault(cfg.busName() != null ? cfg.busName() : DbusConfig.getBusname())
|
||||||
@ -112,7 +112,7 @@ public class App {
|
|||||||
parser.addArgument("--disable-send-log")
|
parser.addArgument("--disable-send-log")
|
||||||
.help("Disable message send log (for resending messages that recipient couldn't decrypt)")
|
.help("Disable message send log (for resending messages that recipient couldn't decrypt)")
|
||||||
.action(Arguments.storeTrue())
|
.action(Arguments.storeTrue())
|
||||||
.setDefault(cfg.disableSendLog() != null ? cfg.disableSendLog() : false);
|
.setDefault(cfg.disableSendLog());
|
||||||
|
|
||||||
parser.epilog(
|
parser.epilog(
|
||||||
"The global arguments are shown with 'signal-cli -h' and need to come before the subcommand, while the subcommand-specific arguments (shown with 'signal-cli SUBCOMMAND -h') need to be given after the subcommand.");
|
"The global arguments are shown with 'signal-cli -h' and need to come before the subcommand, while the subcommand-specific arguments (shown with 'signal-cli SUBCOMMAND -h') need to be given after the subcommand.");
|
||||||
@ -184,20 +184,16 @@ public class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
account = getAccountIfOnlyOne(signalAccountFiles);
|
account = getAccountIfOnlyOne(signalAccountFiles);
|
||||||
|
} else if (!Manager.isValidNumber(account, null)) {
|
||||||
|
throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command instanceof RegistrationCommand registrationCommand) {
|
if (command instanceof RegistrationCommand registrationCommand) {
|
||||||
if (!Manager.isValidNumber(account, null)) {
|
|
||||||
throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
|
|
||||||
}
|
|
||||||
handleRegistrationCommand(registrationCommand, account, signalAccountFiles, commandHandler);
|
handleRegistrationCommand(registrationCommand, account, signalAccountFiles, commandHandler);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command instanceof LocalCommand localCommand) {
|
if (command instanceof LocalCommand localCommand) {
|
||||||
if (!Manager.isValidNumber(account, null) && !Manager.isValidAci(account)) {
|
|
||||||
throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
|
|
||||||
}
|
|
||||||
handleLocalCommand(localCommand, account, signalAccountFiles, commandHandler);
|
handleLocalCommand(localCommand, account, signalAccountFiles, commandHandler);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -334,11 +330,7 @@ public class App {
|
|||||||
) throws CommandException {
|
) throws CommandException {
|
||||||
logger.trace("Loading account file for {}", account);
|
logger.trace("Loading account file for {}", account);
|
||||||
try {
|
try {
|
||||||
if (Manager.isValidAci(account)) {
|
return signalAccountFiles.initManager(account);
|
||||||
return signalAccountFiles.initManagerByAci(account);
|
|
||||||
} else {
|
|
||||||
return signalAccountFiles.initManagerByNumber(account);
|
|
||||||
}
|
|
||||||
} catch (NotRegisteredException e) {
|
} catch (NotRegisteredException e) {
|
||||||
throw new UserErrorException("User " + account + " is not registered.");
|
throw new UserErrorException("User " + account + " is not registered.");
|
||||||
} catch (AccountCheckException ace) {
|
} catch (AccountCheckException ace) {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ public class BaseConfig {
|
|||||||
public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion();
|
public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion();
|
||||||
|
|
||||||
static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
|
static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
|
||||||
.orElse("Signal-Android/8.27.0");
|
.orElse("Signal-Android/8.12.1");
|
||||||
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
|
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
|
||||||
? "signal-cli"
|
? "signal-cli"
|
||||||
: PROJECT_NAME + "/" + PROJECT_VERSION;
|
: PROJECT_NAME + "/" + PROJECT_VERSION;
|
||||||
|
|||||||
@ -113,13 +113,13 @@ public class Main {
|
|||||||
.defaultHelp(false);
|
.defaultHelp(false);
|
||||||
parser.addArgument("-v", "--verbose")
|
parser.addArgument("-v", "--verbose")
|
||||||
.action(Arguments.count())
|
.action(Arguments.count())
|
||||||
.setDefault(config == null || config.verbose() == null ? 0 : config.verbose());
|
.setDefault(config == null ? null : config.verbose());
|
||||||
parser.addArgument("--log-file")
|
parser.addArgument("--log-file")
|
||||||
.type(File.class)
|
.type(File.class)
|
||||||
.setDefault(config == null || config.logFile() == null ? null : new File(config.logFile()));
|
.setDefault(config == null || config.logFile() == null ? null : new File(config.logFile()));
|
||||||
parser.addArgument("--scrub-log")
|
parser.addArgument("--scrub-log")
|
||||||
.action(Arguments.storeTrue())
|
.action(Arguments.storeTrue())
|
||||||
.setDefault(config == null || config.scrubLog() == null ? false : config.scrubLog());
|
.setDefault(config == null ? null : config.scrubLog());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return parser.parseKnownArgs(args, null);
|
return parser.parseKnownArgs(args, null);
|
||||||
|
|||||||
@ -310,8 +310,6 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
|||||||
if (callMessage.hangup().isPresent()) {
|
if (callMessage.hangup().isPresent()) {
|
||||||
var hangupMessage = callMessage.hangup().get();
|
var hangupMessage = callMessage.hangup().get();
|
||||||
writer.println("Hangup message: {}", callIdUnsigned(hangupMessage.id()));
|
writer.println("Hangup message: {}", callIdUnsigned(hangupMessage.id()));
|
||||||
writer.println("- type: {}", hangupMessage.type());
|
|
||||||
writer.println("- device id: {}", hangupMessage.deviceId());
|
|
||||||
}
|
}
|
||||||
if (!callMessage.iceUpdate().isEmpty()) {
|
if (!callMessage.iceUpdate().isEmpty()) {
|
||||||
writer.println("Ice update messages:");
|
writer.println("Ice update messages:");
|
||||||
|
|||||||
@ -52,7 +52,6 @@ public class Commands {
|
|||||||
addCommand(new SendPollTerminateCommand());
|
addCommand(new SendPollTerminateCommand());
|
||||||
addCommand(new SendReactionCommand());
|
addCommand(new SendReactionCommand());
|
||||||
addCommand(new SendReceiptCommand());
|
addCommand(new SendReceiptCommand());
|
||||||
addCommand(new SendStoryCommand());
|
|
||||||
addCommand(new SendSyncRequestCommand());
|
addCommand(new SendSyncRequestCommand());
|
||||||
addCommand(new SendTypingCommand());
|
addCommand(new SendTypingCommand());
|
||||||
addCommand(new SendUnpinMessageCommand());
|
addCommand(new SendUnpinMessageCommand());
|
||||||
@ -61,7 +60,6 @@ public class Commands {
|
|||||||
addCommand(new SubmitRateLimitChallengeCommand());
|
addCommand(new SubmitRateLimitChallengeCommand());
|
||||||
addCommand(new StartChangeNumberCommand());
|
addCommand(new StartChangeNumberCommand());
|
||||||
addCommand(new StartLinkCommand());
|
addCommand(new StartLinkCommand());
|
||||||
addCommand(new TerminateGroupCommand());
|
|
||||||
addCommand(new TrustCommand());
|
addCommand(new TrustCommand());
|
||||||
addCommand(new UnblockCommand());
|
addCommand(new UnblockCommand());
|
||||||
addCommand(new UnregisterCommand());
|
addCommand(new UnregisterCommand());
|
||||||
|
|||||||
@ -79,13 +79,12 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
|||||||
final var groupInviteLink = group.groupInviteLinkUrl();
|
final var groupInviteLink = group.groupInviteLinkUrl();
|
||||||
|
|
||||||
writer.println(
|
writer.println(
|
||||||
"Id: {} Name: {} Description: {} Active: {} Blocked: {} Terminated: {} Members: {} Pending members: {} Requesting members: {} Banned: {} Message expiration: {} Link: {}",
|
"Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Banned: {} Message expiration: {} Link: {}",
|
||||||
group.groupId().toBase64(),
|
group.groupId().toBase64(),
|
||||||
group.title(),
|
group.title(),
|
||||||
group.description(),
|
group.description(),
|
||||||
group.isMember(),
|
group.isMember(),
|
||||||
group.isBlocked(),
|
group.isBlocked(),
|
||||||
group.isTerminated(),
|
|
||||||
resolveMembers(group.members()),
|
resolveMembers(group.members()),
|
||||||
resolveMemberAddress(group.pendingMembers()),
|
resolveMemberAddress(group.pendingMembers()),
|
||||||
resolveMemberAddress(group.requestingMembers()),
|
resolveMemberAddress(group.requestingMembers()),
|
||||||
@ -93,12 +92,11 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
|||||||
group.messageExpirationTimer() == 0 ? "disabled" : group.messageExpirationTimer() + "s",
|
group.messageExpirationTimer() == 0 ? "disabled" : group.messageExpirationTimer() + "s",
|
||||||
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
|
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
|
||||||
} else {
|
} else {
|
||||||
writer.println("Id: {} Name: {} Active: {} Blocked: {} Terminated: {}",
|
writer.println("Id: {} Name: {} Active: {} Blocked: {}",
|
||||||
group.groupId().toBase64(),
|
group.groupId().toBase64(),
|
||||||
group.title(),
|
group.title(),
|
||||||
group.isMember(),
|
group.isMember(),
|
||||||
group.isBlocked(),
|
group.isBlocked());
|
||||||
group.isTerminated());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,8 +133,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
|||||||
group.permissionAddMember().name(),
|
group.permissionAddMember().name(),
|
||||||
group.permissionEditDetails().name(),
|
group.permissionEditDetails().name(),
|
||||||
group.permissionSendMessage().name(),
|
group.permissionSendMessage().name(),
|
||||||
groupInviteLink == null ? null : groupInviteLink.getUrl(),
|
groupInviteLink == null ? null : groupInviteLink.getUrl());
|
||||||
group.isTerminated());
|
|
||||||
}).toList();
|
}).toList();
|
||||||
jsonWriter.write(jsonGroups);
|
jsonWriter.write(jsonGroups);
|
||||||
}
|
}
|
||||||
@ -164,8 +161,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
|||||||
String permissionAddMember,
|
String permissionAddMember,
|
||||||
String permissionEditDetails,
|
String permissionEditDetails,
|
||||||
String permissionSendMessage,
|
String permissionSendMessage,
|
||||||
String groupInviteLink,
|
String groupInviteLink
|
||||||
boolean isTerminated
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private record JsonGroupMemberAddress(String number, String uuid) {}
|
private record JsonGroupMemberAddress(String number, String uuid) {}
|
||||||
|
|||||||
@ -36,7 +36,6 @@ import static org.asamk.signal.util.SendMessageResultUtils.outputResult;
|
|||||||
public class SendCommand implements JsonRpcLocalCommand {
|
public class SendCommand implements JsonRpcLocalCommand {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SendCommand.class);
|
private static final Logger logger = LoggerFactory.getLogger(SendCommand.class);
|
||||||
private static final String BLURHASH_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@ -113,13 +112,6 @@ public class SendCommand implements JsonRpcLocalCommand {
|
|||||||
subparser.addArgument("--voice-note")
|
subparser.addArgument("--voice-note")
|
||||||
.action(Arguments.storeTrue())
|
.action(Arguments.storeTrue())
|
||||||
.help("Mark audio attachments as voice notes. Voice notes are displayed inline in Signal clients.");
|
.help("Mark audio attachments as voice notes. Voice notes are displayed inline in Signal clients.");
|
||||||
subparser.addArgument("--attachment-dimensions")
|
|
||||||
.nargs("*")
|
|
||||||
.help("Specify displayed WIDTHxHEIGHT for each attachment in order. Use an empty string to skip one.");
|
|
||||||
subparser.addArgument("--attachment-blurhash")
|
|
||||||
.nargs("*")
|
|
||||||
.help("Specify a BlurHash (https://blurha.sh) for each attachment in order, which clients show "
|
|
||||||
+ "while it's downloading. Use an empty string to skip one.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -183,13 +175,6 @@ public class SendCommand implements JsonRpcLocalCommand {
|
|||||||
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
|
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
|
||||||
final var voiceNote = Boolean.TRUE.equals(ns.getBoolean("voice-note"));
|
final var voiceNote = Boolean.TRUE.equals(ns.getBoolean("voice-note"));
|
||||||
|
|
||||||
final var dimensionStrings = ns.<String>getList("attachment-dimensions");
|
|
||||||
final var attachmentDimensions = dimensionStrings == null
|
|
||||||
? List.<Message.AttachmentDimensions>of()
|
|
||||||
: parseAttachmentDimensions(dimensionStrings);
|
|
||||||
final var blurHashes = ns.<String>getList("attachment-blurhash");
|
|
||||||
final var attachmentBlurHashes = blurHashes == null ? List.<String>of() : parseAttachmentBlurHashes(blurHashes);
|
|
||||||
|
|
||||||
final var selfNumber = m.getSelfNumber();
|
final var selfNumber = m.getSelfNumber();
|
||||||
|
|
||||||
final var mentionStrings = ns.<String>getList("mention");
|
final var mentionStrings = ns.<String>getList("mention");
|
||||||
@ -264,8 +249,6 @@ public class SendCommand implements JsonRpcLocalCommand {
|
|||||||
try {
|
try {
|
||||||
final var message = new Message(messageText,
|
final var message = new Message(messageText,
|
||||||
attachments,
|
attachments,
|
||||||
attachmentDimensions,
|
|
||||||
attachmentBlurHashes,
|
|
||||||
viewOnce,
|
viewOnce,
|
||||||
voiceNote,
|
voiceNote,
|
||||||
mentions,
|
mentions,
|
||||||
@ -297,46 +280,6 @@ public class SendCommand implements JsonRpcLocalCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Message.AttachmentDimensions> parseAttachmentDimensions(
|
|
||||||
final List<String> dimensionStrings
|
|
||||||
) throws UserErrorException {
|
|
||||||
final var dimensionPattern = Pattern.compile("([1-9]\\d*)x([1-9]\\d*)");
|
|
||||||
final var dimensions = new ArrayList<Message.AttachmentDimensions>();
|
|
||||||
for (final var dimension : dimensionStrings) {
|
|
||||||
if (dimension.isEmpty()) {
|
|
||||||
dimensions.add(null);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final var matcher = dimensionPattern.matcher(dimension);
|
|
||||||
if (!matcher.matches()) {
|
|
||||||
throw new UserErrorException("Invalid attachment dimensions syntax ("
|
|
||||||
+ dimension
|
|
||||||
+ ") expected 'WIDTHxHEIGHT'");
|
|
||||||
}
|
|
||||||
dimensions.add(new Message.AttachmentDimensions(Integer.parseInt(matcher.group(1)),
|
|
||||||
Integer.parseInt(matcher.group(2))));
|
|
||||||
}
|
|
||||||
return dimensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> parseAttachmentBlurHashes(final List<String> blurHashes) throws UserErrorException {
|
|
||||||
for (final var blurHash : blurHashes) {
|
|
||||||
if (!blurHash.isEmpty() && !isValidBlurHash(blurHash)) {
|
|
||||||
throw new UserErrorException("Invalid attachment BlurHash (" + blurHash + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return blurHashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same check the BlurHash decoders make: the first digit fixes the length.
|
|
||||||
private static boolean isValidBlurHash(final String blurHash) {
|
|
||||||
if (blurHash.length() < 6) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final var sizeFlag = BLURHASH_DIGITS.indexOf(blurHash.charAt(0));
|
|
||||||
return blurHash.length() == 4 + 2 * (sizeFlag % 9 + 1) * (sizeFlag / 9 + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Message.Mention> parseMentions(
|
private List<Message.Mention> parseMentions(
|
||||||
final String selfNumber,
|
final String selfNumber,
|
||||||
final List<String> mentionStrings
|
final List<String> mentionStrings
|
||||||
|
|||||||
@ -41,9 +41,7 @@ public class SendPollVoteCommand implements JsonRpcLocalCommand {
|
|||||||
.help("If self is part of recipients/groups send a normal message, not a sync message.")
|
.help("If self is part of recipients/groups send a normal message, not a sync message.")
|
||||||
.action(Arguments.storeTrue());
|
.action(Arguments.storeTrue());
|
||||||
|
|
||||||
subparser.addArgument("--poll-author")
|
subparser.addArgument("--poll-author").help("Specify the number of the author of the poll message.");
|
||||||
.help("Specify the number of the author of the poll message.")
|
|
||||||
.required(true);
|
|
||||||
subparser.addArgument("--poll-timestamp")
|
subparser.addArgument("--poll-timestamp")
|
||||||
.type(long.class)
|
.type(long.class)
|
||||||
.help("Specify the timestamp of the original poll message.")
|
.help("Specify the timestamp of the original poll message.")
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
package org.asamk.signal.commands;
|
|
||||||
|
|
||||||
import net.sourceforge.argparse4j.impl.Arguments;
|
|
||||||
import net.sourceforge.argparse4j.inf.Namespace;
|
|
||||||
import net.sourceforge.argparse4j.inf.Subparser;
|
|
||||||
|
|
||||||
import org.asamk.signal.commands.exceptions.CommandException;
|
|
||||||
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
|
|
||||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
|
||||||
import org.asamk.signal.manager.Manager;
|
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
|
||||||
import org.asamk.signal.manager.api.GroupId;
|
|
||||||
import org.asamk.signal.manager.api.GroupNotFoundException;
|
|
||||||
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
|
||||||
import org.asamk.signal.output.OutputWriter;
|
|
||||||
import org.asamk.signal.util.CommandUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.asamk.signal.util.SendMessageResultUtils.outputResult;
|
|
||||||
|
|
||||||
public class SendStoryCommand implements JsonRpcLocalCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "sendStory";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void attachToSubparser(final Subparser subparser) {
|
|
||||||
subparser.help("Post a story to your Story.");
|
|
||||||
subparser.addArgument("-a", "--attachment")
|
|
||||||
.required(true)
|
|
||||||
.help("Specify the file path to the image or video to post as a story.");
|
|
||||||
subparser.addArgument("--no-replies").action(Arguments.storeTrue()).help("Disable replies on this story.");
|
|
||||||
subparser.addArgument("-g", "--group-id")
|
|
||||||
.help("Specify a group to post the story to. Without this, posts to My Story.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleCommand(
|
|
||||||
final Namespace ns,
|
|
||||||
final Manager m,
|
|
||||||
final OutputWriter outputWriter
|
|
||||||
) throws CommandException {
|
|
||||||
final var attachment = ns.getString("attachment");
|
|
||||||
if (attachment == null || attachment.isEmpty()) {
|
|
||||||
throw new UserErrorException("An attachment is required for sending a story.");
|
|
||||||
}
|
|
||||||
|
|
||||||
final var noReplies = Boolean.TRUE.equals(ns.getBoolean("no-replies"));
|
|
||||||
|
|
||||||
final var groupIdStr = ns.getString("group-id");
|
|
||||||
final var groupId = groupIdStr != null
|
|
||||||
? Optional.of(CommandUtil.getGroupId(groupIdStr))
|
|
||||||
: Optional.<GroupId>empty();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final var results = m.sendStory(attachment, !noReplies, groupId);
|
|
||||||
outputResult(outputWriter, results);
|
|
||||||
} catch (AttachmentInvalidException | IOException e) {
|
|
||||||
throw new UnexpectedErrorException("Failed to send story: " + e.getMessage() + " (" + e.getClass()
|
|
||||||
.getSimpleName() + ")", e);
|
|
||||||
} catch (GroupNotFoundException | NotAGroupMemberException e) {
|
|
||||||
throw new UserErrorException(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -42,8 +42,7 @@ public class SubmitRateLimitChallengeCommand implements JsonRpcLocalCommand {
|
|||||||
throw new IOErrorException("Submit challenge error: " + e.getMessage(), e);
|
throw new IOErrorException("Submit challenge error: " + e.getMessage(), e);
|
||||||
} catch (CaptchaRejectedException e) {
|
} catch (CaptchaRejectedException e) {
|
||||||
throw new CaptchaRejectedErrorException(
|
throw new CaptchaRejectedErrorException(
|
||||||
"Captcha rejected, it may be outdated, already used or solved from a different IP address.",
|
"Captcha rejected, it may be outdated, already used or solved from a different IP address.", e);
|
||||||
e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
package org.asamk.signal.commands;
|
|
||||||
|
|
||||||
import net.sourceforge.argparse4j.inf.Namespace;
|
|
||||||
import net.sourceforge.argparse4j.inf.Subparser;
|
|
||||||
|
|
||||||
import org.asamk.signal.commands.exceptions.CommandException;
|
|
||||||
import org.asamk.signal.commands.exceptions.IOErrorException;
|
|
||||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
|
||||||
import org.asamk.signal.manager.Manager;
|
|
||||||
import org.asamk.signal.manager.api.GroupNotFoundException;
|
|
||||||
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
|
||||||
import org.asamk.signal.output.OutputWriter;
|
|
||||||
import org.asamk.signal.util.CommandUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import static org.asamk.signal.util.SendMessageResultUtils.outputResult;
|
|
||||||
|
|
||||||
public class TerminateGroupCommand implements JsonRpcLocalCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "terminateGroup";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void attachToSubparser(final Subparser subparser) {
|
|
||||||
subparser.help(
|
|
||||||
"Permanently terminate a group for all members. Requires admin privileges; afterwards nobody can send messages or start calls.");
|
|
||||||
subparser.addArgument("-g", "--group-id", "--group").required(true).help("Specify the group ID.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleCommand(
|
|
||||||
final Namespace ns,
|
|
||||||
final Manager m,
|
|
||||||
final OutputWriter outputWriter
|
|
||||||
) throws CommandException {
|
|
||||||
final var groupId = CommandUtil.getGroupId(ns.getString("group-id"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
final var results = m.terminateGroup(groupId);
|
|
||||||
outputResult(outputWriter, results);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IOErrorException("Failed to send message: "
|
|
||||||
+ e.getMessage()
|
|
||||||
+ " ("
|
|
||||||
+ e.getClass().getSimpleName()
|
|
||||||
+ ")", e);
|
|
||||||
} catch (GroupNotFoundException | NotAGroupMemberException e) {
|
|
||||||
throw new UserErrorException("Failed to terminate group: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -113,11 +113,6 @@ public class DbusManagerImpl implements Manager {
|
|||||||
return signal.getSelfNumber();
|
return signal.getSelfNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSelfACI() {
|
|
||||||
return signal.getSelfACI();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, UserStatus> getUserStatus(final Set<String> numbers) throws IOException {
|
public Map<String, UserStatus> getUserStatus(final Set<String> numbers) throws IOException {
|
||||||
final var numbersList = new ArrayList<>(numbers);
|
final var numbersList = new ArrayList<>(numbers);
|
||||||
@ -327,21 +322,6 @@ public class DbusManagerImpl implements Manager {
|
|||||||
group.deleteGroup();
|
group.deleteGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public SendGroupMessageResults terminateGroup(
|
|
||||||
final GroupId groupId
|
|
||||||
) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
|
||||||
final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
|
|
||||||
try {
|
|
||||||
group.terminateGroup();
|
|
||||||
} catch (Signal.Error.GroupNotFound e) {
|
|
||||||
throw new GroupNotFoundException(groupId);
|
|
||||||
} catch (Signal.Error.NotAGroupMember e) {
|
|
||||||
throw new NotAGroupMemberException(groupId, group.Get("org.asamk.Signal.Group", "Name"));
|
|
||||||
}
|
|
||||||
return new SendGroupMessageResults(0, List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
final String name,
|
final String name,
|
||||||
@ -562,11 +542,6 @@ public class DbusManagerImpl implements Manager {
|
|||||||
return new SendMessageResults(timestamp, Map.of());
|
return new SendMessageResults(timestamp, Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public SendMessageResults sendStory(String attachment, boolean allowsReplies, Optional<GroupId> groupId) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
|
public void sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
|
||||||
signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
|
signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
|
||||||
@ -839,7 +814,6 @@ public class DbusManagerImpl implements Manager {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
contactBlocked,
|
contactBlocked,
|
||||||
0,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -887,8 +861,7 @@ public class DbusManagerImpl implements Manager {
|
|||||||
GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
|
GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
|
||||||
GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
|
GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
|
||||||
(boolean) group.get("IsMember").getValue(),
|
(boolean) group.get("IsMember").getValue(),
|
||||||
(boolean) group.get("IsAdmin").getValue(),
|
(boolean) group.get("IsAdmin").getValue());
|
||||||
group.get("IsTerminated") != null && (boolean) group.get("IsTerminated").getValue());
|
|
||||||
} catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
|
} catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -141,11 +141,6 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||||||
return m.getSelfNumber();
|
return m.getSelfNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSelfACI() {
|
|
||||||
return m.getSelfACI();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void subscribeReceive() {
|
public void subscribeReceive() {
|
||||||
if (dbusMessageHandler == null) {
|
if (dbusMessageHandler == null) {
|
||||||
@ -242,8 +237,6 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||||||
try {
|
try {
|
||||||
final var message = new Message(messageText,
|
final var message = new Message(messageText,
|
||||||
attachments,
|
attachments,
|
||||||
List.of(),
|
|
||||||
List.of(),
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
List.of(),
|
List.of(),
|
||||||
@ -411,8 +404,6 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||||||
try {
|
try {
|
||||||
final var message = new Message(messageText,
|
final var message = new Message(messageText,
|
||||||
attachments,
|
attachments,
|
||||||
List.of(),
|
|
||||||
List.of(),
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
List.of(),
|
List.of(),
|
||||||
@ -460,8 +451,6 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||||||
try {
|
try {
|
||||||
final var message = new Message(messageText,
|
final var message = new Message(messageText,
|
||||||
attachments,
|
attachments,
|
||||||
List.of(),
|
|
||||||
List.of(),
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
List.of(),
|
List.of(),
|
||||||
@ -1314,7 +1303,6 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||||||
new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked),
|
new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked),
|
||||||
new DbusProperty<>("IsMember", () -> getGroup().isMember()),
|
new DbusProperty<>("IsMember", () -> getGroup().isMember()),
|
||||||
new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()),
|
new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()),
|
||||||
new DbusProperty<>("IsTerminated", () -> getGroup().isTerminated()),
|
|
||||||
new DbusProperty<>("MessageExpirationTimer",
|
new DbusProperty<>("MessageExpirationTimer",
|
||||||
() -> getGroup().messageExpirationTimer(),
|
() -> getGroup().messageExpirationTimer(),
|
||||||
this::setMessageExpirationTime),
|
this::setMessageExpirationTime),
|
||||||
@ -1382,19 +1370,6 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||||||
updateGroups();
|
updateGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminateGroup() throws Error.Failure {
|
|
||||||
try {
|
|
||||||
m.terminateGroup(groupId);
|
|
||||||
} catch (GroupNotFoundException e) {
|
|
||||||
throw new Error.GroupNotFound(e.getMessage());
|
|
||||||
} catch (NotAGroupMemberException e) {
|
|
||||||
throw new Error.NotAGroupMember(e.getMessage());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new Error.Failure(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addMembers(final List<String> recipients) throws Error.Failure {
|
public void addMembers(final List<String> recipients) throws Error.Failure {
|
||||||
final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
|
final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
|
||||||
|
|||||||
@ -73,15 +73,8 @@ public class HttpServerHandler implements AutoCloseable {
|
|||||||
|
|
||||||
server.start();
|
server.start();
|
||||||
logger.info("Started HTTP server on {}", address);
|
logger.info("Started HTTP server on {}", address);
|
||||||
// If we're listening on any local address (0.0.0.0 or ::), skip Host header validation
|
|
||||||
final var addr = address == null ? null : address.getAddress();
|
|
||||||
if (addr != null && addr.isAnyLocalAddress()) {
|
|
||||||
logger.warn("HTTP server has no authentication; Host header validation DISABLED because listening on {}",
|
|
||||||
address);
|
|
||||||
} else {
|
|
||||||
logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts);
|
logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
@ -115,8 +108,7 @@ public class HttpServerHandler implements AutoCloseable {
|
|||||||
private void handleRpcEndpoint(HttpExchange httpExchange) throws IOException {
|
private void handleRpcEndpoint(HttpExchange httpExchange) throws IOException {
|
||||||
if (!isHostAllowed(httpExchange)) {
|
if (!isHostAllowed(httpExchange)) {
|
||||||
logger.warn("Rejected RPC request with invalid Host header: {} from {}",
|
logger.warn("Rejected RPC request with invalid Host header: {} from {}",
|
||||||
httpExchange.getRequestHeaders().getFirst("Host"),
|
httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress());
|
||||||
httpExchange.getRemoteAddress());
|
|
||||||
sendResponse(421, null, httpExchange);
|
sendResponse(421, null, httpExchange);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -169,8 +161,7 @@ public class HttpServerHandler implements AutoCloseable {
|
|||||||
private void handleEventsEndpoint(HttpExchange httpExchange) throws IOException {
|
private void handleEventsEndpoint(HttpExchange httpExchange) throws IOException {
|
||||||
if (!isHostAllowed(httpExchange)) {
|
if (!isHostAllowed(httpExchange)) {
|
||||||
logger.warn("Rejected Events request with invalid Host header: {} from {}",
|
logger.warn("Rejected Events request with invalid Host header: {} from {}",
|
||||||
httpExchange.getRequestHeaders().getFirst("Host"),
|
httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress());
|
||||||
httpExchange.getRemoteAddress());
|
|
||||||
sendResponse(421, null, httpExchange);
|
sendResponse(421, null, httpExchange);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -265,9 +256,6 @@ public class HttpServerHandler implements AutoCloseable {
|
|||||||
} else {
|
} else {
|
||||||
final var manager = c.getManager(account);
|
final var manager = c.getManager(account);
|
||||||
if (manager == null) {
|
if (manager == null) {
|
||||||
// Account not found by the given identifier (number or ACI/UUID)
|
|
||||||
// Log the available accounts to help debug
|
|
||||||
logger.warn("Account not found for identifier: {}", account);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return List.of(manager);
|
return List.of(manager);
|
||||||
@ -318,18 +306,6 @@ public class HttpServerHandler implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isHostAllowed(final HttpExchange httpExchange) {
|
private boolean isHostAllowed(final HttpExchange httpExchange) {
|
||||||
// If the server is bound to any local address (0.0.0.0 or ::), skip host header validation
|
|
||||||
if (address != null) {
|
|
||||||
final var addr = address.getAddress();
|
|
||||||
if (addr != null && addr.isAnyLocalAddress()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final var hostStr = address.getHostString();
|
|
||||||
if ("0.0.0.0".equals(hostStr) || "::".equals(hostStr)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host");
|
final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host");
|
||||||
if (hostHeader == null || hostHeader.isEmpty()) {
|
if (hostHeader == null || hostHeader.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
package org.asamk.signal.json;
|
package org.asamk.signal.json;
|
||||||
|
|
||||||
|
import io.micronaut.jsonschema.JsonSchema;
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import io.micronaut.jsonschema.JsonSchema;
|
|
||||||
|
|
||||||
@JsonSchema(title = "AdminDelete")
|
@JsonSchema(title = "AdminDelete")
|
||||||
public record JsonAdminDelete(
|
public record JsonAdminDelete(
|
||||||
@Deprecated String targetAuthor, String targetAuthorNumber, String targetAuthorUuid, long targetSentTimestamp
|
@Deprecated String targetAuthor, String targetAuthorNumber, String targetAuthorUuid, long targetSentTimestamp
|
||||||
|
|||||||
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