Merge branch 'master' into get-rate-limit-status

This commit is contained in:
tonycpsu 2026-06-01 22:15:14 -04:00 committed by GitHub
commit 862c73cad1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 1613 additions and 487 deletions

View File

@ -13,22 +13,22 @@ jobs:
build:
strategy:
matrix:
# The "reproducible" entry is used to build the project with the LTS Java version used in reproducible builds script.
# java="25" is the LTS Java version used in reproducible builds script (default in Containerfile).
# More Java versions can be added to test compatibility, eg. "26".
java: ["reproducible", "26"]
java: ["25", "26"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build
run: |
if [ "${{ matrix.java }}" != "reproducible" ]; then
if [ "${{ matrix.java }}" != "25" ]; then
export OVERRIDE_JAVA_VERSION="${{ matrix.java }}"
fi
./reproducible-builds/build.sh
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-${{ matrix.java }}-${{ github.job }}
name: signal-cli-archive-${{ matrix.java }}
path: dist/*
build-client:

View File

@ -12,10 +12,11 @@ env:
IMAGE_REGISTRY: ghcr.io/asamk
REGISTRY_USER: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ github.token }}
ARCHIVE_JAVA_VERSION: 25
jobs:
build:
uses: AsamK/signal-cli/.github/workflows/build.yml@master
uses: ./.github/workflows/build.yml
release:
needs: build
@ -31,7 +32,7 @@ jobs:
- name: Get signal-cli version
id: version
run: |
mv ./signal-cli-reproducible-build/* .
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Create release
@ -87,7 +88,7 @@ jobs:
- name: Move archive file
run: |
tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}.tar.gz
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}.tar.gz
mkdir -p build/install/
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
@ -127,7 +128,7 @@ jobs:
- name: Move archive file
run: |
tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
mkdir -p build/native/nativeCompile/
mv signal-cli build/native/nativeCompile/
chmod +x build/native/nativeCompile/signal-cli
@ -168,7 +169,7 @@ jobs:
- name: Move archive file
run: |
tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
mkdir -p client/target/release/
mv signal-cli-client client/target/release/
chmod +x client/target/release/signal-cli-client

View File

@ -5,10 +5,39 @@
### Added
- `getRateLimitStatus` JSON-RPC/CLI command returning the current rate-limit state for the account (active flag, `retryAfterSeconds`, `challengeToken`, `expiresAtEpochSeconds`). Useful for admin UIs, monitoring, and clients that want to query current state without triggering a send.
## [0.14.4] - 2026-05-23
### Added
- Support for a global configuration file to set system-wide defaults
### Fixed
- Group admins can now see profile information for users requesting to join groups.
- Storage sync with unregistered contacts fixed
- Incoming messages are validated more accurately, fixing receiving messages from new contacts
### Improved
- Some security and stability improvements, including HTTP HOST header validation and safer temporary file handling.
## [0.14.3] - 2026-04-22
### Fixed
- Fix sender key re-distribution on every group message (Thanks @meinecke)
### Improved
- Performance improvement when assigning admin role to multiple group members
- Increase disconnect timeout for websocket connections
- Release builds are now reproducible
### Changed
- Send message results now surface server-advised retry time for plain rate-limit (HTTP 413) failures, not only for proof-required challenges. The `retryAfterSeconds` field in JSON-RPC `SendMessageResult` is populated whenever the server sends a `Retry-After` header. The canonical way to distinguish proof-required failures remains `token != null`. Text output includes "retry after N seconds" when known.
- Add distinct JSON-RPC error code (6) for captcha rejection (Thanks @tonycpsu)
- No longer sends busy call response to allow linked devices to accept call
## [0.14.2] - 2026-04-04

View File

@ -10,7 +10,7 @@ plugins {
allprojects {
group = "org.asamk"
version = "0.14.3-SNAPSHOT"
version = "0.14.5-SNAPSHOT"
}
java {
@ -102,7 +102,7 @@ dependencies {
implementation(libs.micronaut.json.schema.annotations)
if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) {
implementation(libs.micronaut.json.schema.generator)
}
}
implementation(project(":libsignal-cli"))
testImplementation(libs.junit.jupiter)

View File

@ -7,11 +7,11 @@ plugins {
}
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
compilerOptions.jvmTarget.set(JvmTarget.JVM_24)
compilerOptions.jvmTarget.set(JvmTarget.JVM_25)
}
java {
targetCompatibility = JavaVersion.VERSION_24
targetCompatibility = JavaVersion.VERSION_25
}
repositories {

323
client/Cargo.lock generated
View File

@ -4,9 +4,9 @@ version = 4
[[package]]
name = "anstream"
version = "0.6.21"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
@ -19,15 +19,15 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
@ -83,9 +83,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.11.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bytes"
@ -95,9 +95,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
version = "1.2.56"
version = "1.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"shlex",
@ -117,9 +117,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.5.60"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
@ -127,9 +127,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
@ -140,9 +140,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.55"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
@ -152,15 +152,15 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "combine"
@ -326,9 +326,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.16.1"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heck"
@ -377,9 +377,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hyper"
version = "1.8.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
"atomic-waker",
"bytes",
@ -391,7 +391,6 @@ dependencies = [
"httparse",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
@ -399,16 +398,15 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.7"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
@ -436,12 +434,13 @@ dependencies = [
[[package]]
name = "icu_collections"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
@ -449,9 +448,9 @@ dependencies = [
[[package]]
name = "icu_locale_core"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
"displaydoc",
"litemap",
@ -462,9 +461,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@ -476,15 +475,15 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]]
name = "icu_properties"
version = "2.1.2"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [
"icu_collections",
"icu_locale_core",
@ -496,15 +495,15 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.1.2"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]]
name = "icu_provider"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
"displaydoc",
"icu_locale_core",
@ -538,9 +537,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.13.0"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
@ -554,9 +553,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jni"
@ -567,7 +566,7 @@ dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"jni-sys 0.3.1",
"log",
"thiserror 1.0.69",
"walkdir",
@ -576,9 +575,31 @@ dependencies = [
[[package]]
name = "jni-sys"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
dependencies = [
"jni-sys 0.4.1",
]
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "jsonrpsee"
@ -668,9 +689,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.182"
version = "0.2.185"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
name = "linux-raw-sys"
@ -680,9 +701,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "log"
@ -698,9 +719,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mio"
version = "1.1.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi",
@ -709,9 +730,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.3"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
@ -757,26 +778,20 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "potential_utf"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
"zerovec",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
@ -792,9 +807,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
@ -815,9 +830,9 @@ dependencies = [
[[package]]
name = "rustc-hash"
version = "2.1.1"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustix"
@ -834,9 +849,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.37"
version = "0.23.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
dependencies = [
"log",
"once_cell",
@ -897,9 +912,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.10"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"ring",
"rustls-pki-types",
@ -917,9 +932,9 @@ dependencies = [
[[package]]
name = "schannel"
version = "0.1.28"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
@ -1026,12 +1041,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@ -1082,12 +1097,12 @@ dependencies = [
[[package]]
name = "terminal_size"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@ -1132,9 +1147,9 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [
"displaydoc",
"zerovec",
@ -1142,9 +1157,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.49.0"
version = "1.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776"
dependencies = [
"bytes",
"libc",
@ -1157,9 +1172,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
@ -1202,18 +1217,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.23.10+spec-1.0.0"
version = "0.25.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
dependencies = [
"indexmap",
"toml_datetime",
@ -1223,9 +1238,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow",
]
@ -1360,14 +1375,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
dependencies = [
"webpki-root-certs 1.0.6",
"webpki-root-certs 1.0.7",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.6"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
dependencies = [
"rustls-pki-types",
]
@ -1414,15 +1429,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@ -1456,30 +1462,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
@ -1492,12 +1481,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@ -1510,12 +1493,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@ -1528,24 +1505,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@ -1558,12 +1523,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@ -1576,12 +1535,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@ -1594,12 +1547,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@ -1612,32 +1559,26 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
dependencies = [
"memchr",
]
[[package]]
name = "writeable"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "yoke"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
dependencies = [
"stable_deref_trait",
"yoke-derive",
@ -1646,9 +1587,9 @@ dependencies = [
[[package]]
name = "yoke-derive"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
@ -1658,18 +1599,18 @@ dependencies = [
[[package]]
name = "zerofrom"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
@ -1685,9 +1626,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
dependencies = [
"displaydoc",
"yoke",
@ -1696,9 +1637,9 @@ dependencies = [
[[package]]
name = "zerovec"
version = "0.11.5"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [
"yoke",
"zerofrom",
@ -1707,9 +1648,9 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",

View File

@ -45,6 +45,12 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="0.14.4" date="2026-05-23">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
</release>
<release version="0.14.3" date="2026-04-22">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.3</url>
</release>
<release version="0.14.2" date="2026-04-04">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
</release>

View File

@ -1,11 +1,12 @@
[versions]
slf4j = "2.0.17"
junit = "6.0.2"
micronaut-json-schema = "2.0.0-M6"
micronaut-core = "4.9.3"
slf4j = "2.0.18"
junit = "6.1.0"
micronaut-json-schema = "2.0.1"
micronaut-core = "5.0.0"
signal-service = "2.15.3_unofficial_147"
[libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83"
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.20.2"
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
@ -18,8 +19,8 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.5.32"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_143"
sqlite = "org.xerial:sqlite-jdbc:3.51.2.0"
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
sqlite = "org.xerial:sqlite-jdbc:3.53.1.0"
hikari = "com.zaxxer:HikariCP:7.0.2"
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

Binary file not shown.

View File

@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

31
gradlew.bat vendored
View File

@ -23,8 +23,8 @@
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@ -65,7 +65,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@ -73,21 +73,10 @@ goto fail
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%

View File

@ -18,9 +18,9 @@ val libsignalClientPath = project.findProperty("libsignal_client_path")?.toStrin
dependencies {
if (libsignalClientPath == null) {
implementation(libs.signalservice)
implementation(libs.signalnetwork)
} else {
implementation(libs.signalservice) {
implementation(libs.signalnetwork) {
exclude(group = "org.signal", module = "libsignal-client")
}
implementation(files(libsignalClientPath))

View File

@ -266,7 +266,7 @@ public interface Manager extends Closeable {
RecipientIdentifier.Single recipient
) throws IOException;
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
SendMessageResults sendMessageRequestResponse(
MessageEnvelope.Sync.MessageRequestResponse.Type type,

View File

@ -4,7 +4,6 @@ import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.util.MimeUtils;
import org.signal.core.models.ServiceId;
import org.signal.libsignal.metadata.ProtocolException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@ -157,7 +156,7 @@ public record MessageEnvelope(
dataMessage.getExpiresInSeconds(),
dataMessage.isExpirationUpdate(),
dataMessage.isViewOnce(),
dataMessage.isEndSession(),
false,
dataMessage.isProfileKeyUpdate(),
dataMessage.getProfileKey().isPresent(),
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
@ -1028,7 +1027,7 @@ public record MessageEnvelope(
final AttachmentFileProvider fileProvider,
Exception exception
) {
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
final var serviceId = envelope.getSourceServiceId();
final var source = !envelope.isUnidentifiedSender() && serviceId != null
? recipientResolver.resolveRecipient(serviceId)
: envelope.isUnidentifiedSender() && content != null

View File

@ -25,6 +25,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.protocol.util.KeyHelper;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
@ -37,7 +38,6 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;

View File

@ -34,8 +34,10 @@ public class AttachmentHelper {
private final SignalDependencies dependencies;
private final AttachmentStore attachmentStore;
private final Context context;
public AttachmentHelper(final Context context) {
this.context = context;
this.dependencies = context.getDependencies();
this.attachmentStore = context.getAttachmentStore();
}
@ -92,6 +94,21 @@ public class AttachmentHelper {
final boolean voiceNote
) throws AttachmentInvalidException {
try {
// Reject local files that point into the signal-cli data directory
if (attachment != null && !attachment.startsWith("data:")) {
try {
final var file = new File(attachment);
final var canonical = file.getCanonicalFile();
final var dataPath = context.getAccount().getDataPath().getCanonicalFile();
if (canonical.toPath().startsWith(dataPath.toPath())) {
throw new AttachmentInvalidException(attachment,
new IOException("Attaching files from the signal-cli data directory is not allowed"));
}
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
final var streamDetailsAndFileName = Utils.createStreamDetails(attachment);
final var streamDetails = streamDetailsAndFileName.first();
final var uploadSpec = getResumableUploadSpec(streamDetails);
@ -109,7 +126,7 @@ public class AttachmentHelper {
final var streamLength = streamDetails.getLength();
final var ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(
streamLength));
return dependencies.getMessageSender().getResumableUploadSpec(ciphertextLength);
return dependencies.getCdnService().getResumableUploadSpecBlocking(ciphertextLength);
}
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {

View File

@ -105,6 +105,8 @@ public class CallManager implements AutoCloseable {
recipientAddress,
recipientId);
activeCalls.put(callId, state);
dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
fireCallEvent(state, null);
// Spawn call tunnel binary and connect control channel
@ -197,11 +199,6 @@ public class CallManager implements AutoCloseable {
if (callEventListeners.isEmpty()) {
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
callIdUnsigned(callId));
final var result = sendBusyMessage(callId, recipientId, deviceId);
if (!result.isSuccess()) {
logger.warn("Failed to send busy for unhandled call {}", callIdUnsigned(callId));
}
return;
}
@ -701,6 +698,8 @@ public class CallManager implements AutoCloseable {
private void endCall(final long callId, final String reason) {
var state = activeCalls.remove(callId);
dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
if (state == null) return;
state.state = CallInfo.State.ENDED;

View File

@ -558,16 +558,24 @@ public class GroupHelper {
private void storeProfileKeysFromMembers(final DecryptedGroup group) {
for (var member : group.members) {
final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
final var profileStore = account.getProfileStore();
if (profileStore.getProfileKey(recipientId) != null) {
// We already have a profile key, not updating it from a non-authoritative source
continue;
}
try {
profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray()));
} catch (InvalidInputException ignored) {
}
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
}
for (var member : group.requestingMembers) {
final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
}
}
private void storeProfileKeyIfMissing(final ServiceId serviceId, final byte[] profileKeyBytes) {
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
final var profileStore = account.getProfileStore();
if (profileStore.getProfileKey(recipientId) != null) {
// We already have a profile key, not updating it from a non-authoritative source
return;
}
try {
profileStore.storeProfileKey(recipientId, new ProfileKey(profileKeyBytes));
} catch (InvalidInputException ignored) {
}
}

View File

@ -21,6 +21,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.signal.storageservice.storage.protos.groups.AccessControl;
import org.signal.storageservice.storage.protos.groups.GroupChange;
import org.signal.storageservice.storage.protos.groups.GroupChangeResponse;
@ -43,7 +44,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
import java.io.IOException;

View File

@ -109,8 +109,8 @@ public final class IncomingMessageHandler {
SignalServiceContent content = null;
if (!envelope.isReceipt()) {
account.getIdentityKeyStore().setRetryingDecryption(true);
final var destination = getDestination(envelope).serviceId();
try {
final var destination = getDestination(envelope).serviceId();
final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
@ -140,15 +140,30 @@ public final class IncomingMessageHandler {
final Manager.ReceiveMessageHandler handler
) {
final var actions = new ArrayList<HandleAction>();
if (envelope.isPreKeySignalMessage()) {
actions.add(RefreshPreKeysAction.create());
}
SignalServiceContent content = null;
Exception exception = null;
envelope.getSourceServiceId().map(ServiceId::parseOrNull)
// Store uuid if we don't have it already
// uuid in envelope is sent by server
.ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId));
if (envelope.getSourceServiceId() != null) {
// Store uuid if we don't have it already
// uuid in envelope is sent by server
account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId());
}
if (!envelope.isReceipt()) {
final var destination = getDestination(envelope).serviceId();
try {
final var destination = getDestination(envelope).serviceId();
if (destination == account.getPni() && envelope.getSourceServiceId() == null) {
throw new InvalidMessageException(
"Got a sealed sender message to our PNI? Invalid message, ignoring.");
}
if (envelope.getSourceServiceId() instanceof ServiceId.PNI
&& envelope.getProto().type != Envelope.Type.SERVER_DELIVERY_RECEIPT) {
throw new InvalidMessageException("Got a message from a PNI that was not a SERVER_DELIVERY_RECEIPT.");
}
final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
@ -173,7 +188,13 @@ public final class IncomingMessageHandler {
logger.debug("Received invalid message from blocked contact, ignoring.");
} else {
var serviceId = ServiceId.parseOrNull(e.getSender());
if (serviceId != null) {
ServiceId destination;
try {
destination = getDestination(envelope).serviceId();
} catch (InvalidMessageException ex) {
destination = null;
}
if (serviceId != null && destination != null) {
final var isSelf = sender.equals(account.getSelfRecipientId())
&& e.getSenderDevice() == account.getDeviceId();
logger.debug("Received invalid message, queuing renew session action.");
@ -311,7 +332,12 @@ public final class IncomingMessageHandler {
final var sender = senderDeviceAddress.recipientId();
final var senderServiceId = senderDeviceAddress.serviceId();
final var senderDeviceId = senderDeviceAddress.deviceId();
final var destination = getDestination(envelope);
final DeviceAddress destination;
try {
destination = getDestination(envelope);
} catch (InvalidMessageException e) {
throw new AssertionError(e);
}
if (account.getPni().equals(destination.serviceId)) {
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true);
@ -874,11 +900,6 @@ public final class IncomingMessageHandler {
final var selfAddress = isSync ? source : destination;
final var conversationPartnerAddress = isSync ? destination : source;
if (conversationPartnerAddress != null && message.isEndSession()) {
account.getAccountData(selfAddress.serviceId())
.getSessionStore()
.deleteAllSessions(conversationPartnerAddress.serviceId());
}
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
if (message.getGroupContext().isPresent()) {
final var groupContext = message.getGroupContext().get();
@ -1047,7 +1068,7 @@ public final class IncomingMessageHandler {
}
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
final var serviceId = envelope.getSourceServiceId();
if (!envelope.isUnidentifiedSender() && serviceId != null) {
return new SignalServiceAddress(serviceId);
} else if (content != null) {
@ -1058,7 +1079,7 @@ public final class IncomingMessageHandler {
}
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
final var serviceId = envelope.getSourceServiceId();
if (!envelope.isUnidentifiedSender() && serviceId != null) {
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId),
serviceId,
@ -1070,10 +1091,13 @@ public final class IncomingMessageHandler {
}
}
private DeviceAddress getDestination(SignalServiceEnvelope envelope) {
private DeviceAddress getDestination(SignalServiceEnvelope envelope) throws InvalidMessageException {
final var destination = envelope.getDestinationServiceId();
if (destination == null || destination.isUnknown()) {
return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
throw new InvalidMessageException("Missing destination");
}
if (!account.getAci().equals(destination) && !account.getPni().equals(destination)) {
throw new InvalidMessageException("Message not intended for this account");
}
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
destination,

View File

@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.InvalidKeyIdException;
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
import org.signal.libsignal.protocol.state.PreKeyRecord;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResultUtil;
@ -16,7 +17,6 @@ import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException;
import java.util.List;
@ -84,7 +84,8 @@ public class PreKeyHelper {
) throws IOException {
OneTimePreKeyCounts preKeyCounts;
try {
preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
preKeyCounts = handleResponseException(dependencies.getKeysApi()
.getAvailablePreKeyCountsSync(serviceIdType));
} catch (AuthorizationFailedException e) {
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
preKeyCounts = new OneTimePreKeyCounts(0, 0);
@ -145,7 +146,7 @@ public class PreKeyHelper {
kyberPreKeyRecords);
var needsReset = false;
try {
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeysSync(preKeyUpload));
try {
if (preKeyRecords != null) {
account.addPreKeys(serviceIdType, preKeyRecords);

View File

@ -21,6 +21,7 @@ import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.network.exceptions.PushNetworkException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResultUtil;
@ -30,7 +31,6 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;

View File

@ -9,7 +9,6 @@ import org.asamk.signal.manager.jobs.CleanOldPreKeysJob;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.signal.core.models.ServiceId;
import org.signal.core.models.ServiceId.ACI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -150,10 +149,10 @@ public class ReceiveHelper {
for (final var it : batch) {
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
it.getServerDeliveredTimestamp());
final var recipientId = envelope1.getSourceServiceId()
.map(ServiceId::parseOrNull)
.map(s -> account.getRecipientResolver().resolveRecipient(s))
.orElse(null);
final var sourceServiceId = envelope1.getSourceServiceId();
final var recipientId = sourceServiceId == null
? null
: account.getRecipientResolver().resolveRecipient(sourceServiceId);
logger.trace("Storing new message from {}", recipientId);
// store message on disk, before acknowledging receipt to the server
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
@ -238,7 +237,7 @@ public class ReceiveHelper {
if (exception instanceof UntrustedIdentityException) {
logger.debug("Keeping message with untrusted identity in message cache");
final var address = ((UntrustedIdentityException) exception).getSender();
if (envelope.getSourceServiceId().isEmpty() && address.aci().isPresent()) {
if (envelope.getSourceServiceId() == null && address.aci().isPresent()) {
final var recipientId = account.getRecipientResolver()
.resolveRecipient(ACI.parseOrThrow(address.aci().get()));
try {
@ -292,7 +291,7 @@ public class ReceiveHelper {
cachedMessage.delete();
return null;
}
if (envelope.getSourceServiceId().isEmpty()) {
if (envelope.getSourceServiceId() == null) {
final var identifier = ((UntrustedIdentityException) exception).getSender();
final var recipientId = account.getRecipientResolver()
.resolveRecipient(new RecipientAddress(identifier));

View File

@ -11,13 +11,13 @@ import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException;
import java.util.Collection;

View File

@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.GroupIdV1;
import org.asamk.signal.manager.api.GroupIdV2;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.SignalAccount;
@ -17,6 +18,9 @@ import org.asamk.signal.manager.util.KeyUtils;
import org.signal.core.models.storageservice.StorageKey;
import org.signal.core.util.SetUtil;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.network.service.StorageServiceService;
import org.signal.network.service.StorageServiceService.ManifestIfDifferentVersionResult;
import org.signal.network.service.StorageServiceService.WriteStorageRecordsResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
@ -25,9 +29,6 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
@ -38,6 +39,7 @@ import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -211,20 +213,23 @@ public class StorageHelper {
remoteOnlyRecords.size());
}
// This logic is wrong, records should only be deleted if they're deleted remotely, not if the remote record is updated
// if (!idDifference.localOnlyIds().isEmpty()) {
// final var updated = account.getRecipientStore()
// .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
// idDifference.localOnlyIds());
//
// if (updated > 0) {
// logger.warn(
// "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
// updated);
// }
// }
//
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
final var listListPair = processKnownRecords(connection, remoteOnlyRecords);
final var unknownInserts = listListPair.first();
final var updatedStorageIds = listListPair.second();
final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds());
updatedStorageIds.forEach(oldUnregisteredLocalOnlyIds::remove);
if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore()
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
oldUnregisteredLocalOnlyIds);
if (updated > 0) {
logger.warn(
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
updated);
}
}
final var unknownDeletes = idDifference.localOnlyIds()
.stream()
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
@ -480,13 +485,13 @@ public class StorageHelper {
private Map<GroupIdV1, StorageId> generateGroupV1StorageIds(List<GroupIdV1> groupIds) {
return groupIds.stream()
.collect(Collectors.toMap(recipientId -> recipientId,
recipientId -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
_ -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
}
private Map<GroupIdV2, StorageId> generateGroupV2StorageIds(List<GroupIdV2> groupIds) {
return groupIds.stream()
.collect(Collectors.toMap(recipientId -> recipientId,
recipientId -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
_ -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
}
private void storeManifestLocally(
@ -504,7 +509,7 @@ public class StorageHelper {
final var result = dependencies.getStorageServiceRepository()
.readStorageRecords(storageKey, manifest.recordIkm, storageIds);
return switch (result) {
case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
case StorageServiceService.StorageRecordResult.DecryptionError decryptionError -> {
if (decryptionError.getException() instanceof InvalidKeyException) {
logger.warn("Failed to read storage records, ignoring.");
yield List.of();
@ -514,11 +519,11 @@ public class StorageHelper {
throw new IOException(decryptionError.getException());
}
}
case StorageServiceRepository.StorageRecordResult.NetworkError networkError ->
case StorageServiceService.StorageRecordResult.NetworkError networkError ->
throw networkError.getException();
case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError ->
case StorageServiceService.StorageRecordResult.StatusCodeError statusCodeError ->
throw statusCodeError.getException();
case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords();
case StorageServiceService.StorageRecordResult.Success success -> success.getRecords();
default -> throw new IllegalStateException("Unexpected value: " + result);
};
}
@ -630,16 +635,17 @@ public class StorageHelper {
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
}
private List<StorageId> processKnownRecords(
private Pair<List<StorageId>, List<StorageId>> processKnownRecords(
final Connection connection,
List<SignalStorageRecord> records
) throws SQLException {
final var unknownRecords = new ArrayList<StorageId>();
final var processedRecords = new ArrayList<StorageId>();
final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor());
final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection);
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
for (final var record : records) {
if (record.getProto().account != null) {
@ -662,8 +668,12 @@ public class StorageHelper {
unknownRecords.add(record.getId());
}
}
processedRecords.addAll(accountRecordProcessor.getUpdatedStorageIds());
processedRecords.addAll(groupV1RecordProcessor.getUpdatedStorageIds());
processedRecords.addAll(groupV2RecordProcessor.getUpdatedStorageIds());
processedRecords.addAll(contactRecordProcessor.getUpdatedStorageIds());
return unknownRecords;
return new Pair<>(unknownRecords, processedRecords);
}
/**

View File

@ -160,7 +160,7 @@ public class SyncHelper {
try {
try (OutputStream fos = new FileOutputStream(contactsFile)) {
var out = new DeviceContactsOutputStream(fos, true, true);
var out = new DeviceContactsOutputStream(fos);
for (var contactPair : account.getContactStore().getContacts()) {
final var recipientId = contactPair.first();
final var contact = contactPair.second();

View File

@ -102,6 +102,7 @@ import org.signal.core.util.Base64;
import org.signal.core.util.Hex;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@ -118,7 +119,6 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.util.Util;
@ -195,6 +195,7 @@ public class ManagerImpl implements Manager {
userAgent,
account.getCredentialsProvider(),
account.getSignalServiceDataStore(),
account.getDeviceId(),
executor,
sessionLock);
final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
@ -1131,30 +1132,26 @@ public class ManagerImpl implements Manager {
}
@Override
public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
try {
return sendMessage(messageBuilder,
recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()),
false);
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
throw new AssertionError(e);
} finally {
for (var recipient : recipients) {
final RecipientId recipientId;
try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
} catch (UnregisteredRecipientException e) {
continue;
}
final var serviceId = context.getAccount()
.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId)
.serviceId();
if (serviceId.isPresent()) {
account.getAccountData(ServiceIdType.ACI).getSessionStore().deleteAllSessions(serviceId.get());
}
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
for (var recipient : recipients) {
final RecipientId recipientId;
try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
} catch (UnregisteredRecipientException e) {
continue;
}
final var recipientAddress = context.getAccount()
.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId);
final var aciSessionStore = account.getAccountData(ServiceIdType.ACI).getSessionStore();
final var pniSessionStore = account.getAccountData(ServiceIdType.PNI).getSessionStore();
if (recipientAddress.aci().isPresent()) {
aciSessionStore.archiveSessions(recipientAddress.aci().get());
pniSessionStore.archiveSessions(recipientAddress.aci().get());
}
if (recipientAddress.pni().isPresent()) {
aciSessionStore.archiveSessions(recipientAddress.pni().get());
pniSessionStore.archiveSessions(recipientAddress.pni().get());
}
}
}

View File

@ -231,6 +231,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
userAgent,
account.getCredentialsProvider(),
account.getSignalServiceDataStore(),
0,
null,
new ReentrantSignalSessionLock());
handleResponseException(dependencies.getAccountApi()

View File

@ -5,7 +5,18 @@ import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.network.api.AttachmentApi;
import org.signal.network.api.CallingApi;
import org.signal.network.api.CdsApi;
import org.signal.network.api.CertificateApi;
import org.signal.network.api.LinkDeviceApi;
import org.signal.network.api.RateLimitChallengeApi;
import org.signal.network.api.UsernameApi;
import org.signal.network.rest.SignalRestClient;
import org.signal.network.service.CdnService;
import org.signal.network.service.StorageServiceService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
@ -14,27 +25,20 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.account.AccountApi;
import org.whispersystems.signalservice.api.attachment.AttachmentApi;
import org.whispersystems.signalservice.api.calling.CallingApi;
import org.whispersystems.signalservice.api.cds.CdsApi;
import org.whispersystems.signalservice.api.certificate.CertificateApi;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.keys.KeysApi;
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
import org.whispersystems.signalservice.api.keys.PreKeyRepository;
import org.whispersystems.signalservice.api.message.MessageApi;
import org.whispersystems.signalservice.api.profiles.ProfileApi;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi;
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
import org.whispersystems.signalservice.api.username.UsernameApi;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
@ -61,6 +65,7 @@ public class SignalDependencies {
private final String userAgent;
private final CredentialsProvider credentialsProvider;
private final SignalServiceDataStore dataStore;
private final int deviceId;
private final ExecutorService executor;
private final SignalSessionLock sessionLock;
@ -82,6 +87,11 @@ public class SignalDependencies {
private KeysApi keysApi;
private GroupsV2Operations groupsV2Operations;
private ClientZkOperations clientZkOperations;
private ProfileService profileService;
private ProfileApi profileApi;
private CdnService cdnService;
private PreKeyRepository preKeyRepository;
private SignalRestClient signalRestClient;
private PushServiceSocket pushServiceSocket;
private Network libSignalNetwork;
@ -91,14 +101,13 @@ public class SignalDependencies {
private SignalServiceMessageSender messageSender;
private List<SecureValueRecovery> secureValueRecovery;
private ProfileService profileService;
private ProfileApi profileApi;
SignalDependencies(
final ServiceEnvironmentConfig serviceEnvironmentConfig,
final String userAgent,
final CredentialsProvider credentialsProvider,
final SignalServiceDataStore dataStore,
final int deviceId,
final ExecutorService executor,
final SignalSessionLock sessionLock
) {
@ -106,6 +115,7 @@ public class SignalDependencies {
this.userAgent = userAgent;
this.credentialsProvider = credentialsProvider;
this.dataStore = dataStore;
this.deviceId = deviceId;
this.executor = executor;
this.sessionLock = sessionLock;
}
@ -243,8 +253,8 @@ public class SignalDependencies {
getPushServiceSocket()));
}
public StorageServiceRepository getStorageServiceRepository() {
return new StorageServiceRepository(getStorageServiceApi());
public StorageServiceService getStorageServiceRepository() {
return new StorageServiceService(getStorageServiceApi());
}
public CertificateApi getCertificateApi() {
@ -326,12 +336,33 @@ public class SignalDependencies {
() -> messageReceiver = new SignalServiceMessageReceiver(getPushServiceSocket()));
}
private SignalRestClient getSignalRestClient() {
return getOrCreate(() -> signalRestClient,
() -> signalRestClient = new SignalRestClient(serviceEnvironmentConfig.signalServiceConfiguration(),
userAgent,
credentialsProvider,
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
}
public CdnService getCdnService() {
return getOrCreate(() -> cdnService,
() -> cdnService = new CdnService(getSignalRestClient(), getAttachmentApi()));
}
public PreKeyRepository getPreKeyRepository() {
final SignalProtocolAddress localProtocolAddress = credentialsProvider.getAci().toProtocolAddress(deviceId);
return getOrCreate(() -> preKeyRepository,
() -> preKeyRepository = new PreKeyRepository(getKeysApi(),
dataStore.aci(),
localProtocolAddress,
Runnable::run));
}
public SignalServiceMessageSender getMessageSender() {
return getOrCreate(() -> messageSender,
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
dataStore,
sessionLock,
getAttachmentApi(),
getMessageApi(),
getKeysApi(),
Optional.empty(),
@ -339,8 +370,7 @@ public class SignalDependencies {
ServiceConfig.MAX_ENVELOPE_SIZE,
ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE,
() -> true,
true,
true));
getPreKeyRepository()));
}
public List<SecureValueRecovery> getSecureValueRecovery() {
@ -368,7 +398,10 @@ public class SignalDependencies {
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots());
final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164());
final var serviceId = serviceIdType == ServiceIdType.ACI
? credentialsProvider.getAci()
: credentialsProvider.getPni();
final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164());
final var deviceId = credentialsProvider.getDeviceId();
return new SignalServiceCipher(address,
deviceId,

View File

@ -1,8 +1,9 @@
package org.asamk.signal.manager.internal;
import org.jetbrains.annotations.NotNull;
import org.signal.network.util.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.Preconditions;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
@ -94,6 +95,14 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor {
return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives();
}
@Override
public void onReceivedAlerts(@NotNull final String[] strings, final boolean b) {
if (strings.length == 0) {
return;
}
logger.info("Received alerts: {}", String.join(", ", strings));
}
/**
* 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.

View File

@ -44,7 +44,8 @@ public class AttachmentStore {
}
public StreamDetails retrieveAttachment(final String id) throws IOException {
final var attachmentFile = new File(attachmentsPath, id);
final var safeId = sanitizeId(id);
final var attachmentFile = new File(attachmentsPath, safeId);
return Utils.createStreamDetailsFromFile(attachmentFile);
}
@ -61,7 +62,8 @@ public class AttachmentStore {
Optional<String> contentType
) {
final var extension = getAttachmentExtension(filename, contentType);
return new File(attachmentsPath, attachmentId.toString() + extension + ".preview");
final var safe = sanitizeId(attachmentId.toString());
return new File(attachmentsPath, safe + extension + ".preview");
}
private File getAttachmentFile(
@ -70,7 +72,15 @@ public class AttachmentStore {
Optional<String> contentType
) {
final var extension = getAttachmentExtension(filename, contentType);
return new File(attachmentsPath, attachmentId.toString() + extension);
final var safe = sanitizeId(attachmentId.toString());
return new File(attachmentsPath, safe + extension);
}
private static String sanitizeId(final String id) {
if (id == null) {
return "";
}
return id.replaceAll("[^A-Za-z0-9_.-]", "_");
}
private static String getAttachmentExtension(final Optional<String> filename, final Optional<String> contentType) {

View File

@ -192,6 +192,10 @@ public class SignalAccount implements Closeable {
this.lock = lock;
}
public File getDataPath() {
return dataPath;
}
public static SignalAccount load(
File dataPath,
String accountPath,

View File

@ -878,7 +878,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
final Connection connection,
final List<StorageId> storageIds
final Collection<StorageId> storageIds
) throws SQLException {
final var sql = (
"""

View File

@ -403,7 +403,7 @@ public class SessionStore implements SignalServiceSessionStore {
}
private static boolean isActive(SessionRecord record) {
return record != null && record.hasSenderChain();
return record != null && record.hasSenderChain(0.0);
}
record Key(String address, int deviceId) {}

View File

@ -6,7 +6,9 @@ import org.whispersystems.signalservice.api.storage.SignalRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
@ -24,6 +26,7 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
private static final Logger logger = LoggerFactory.getLogger(DefaultStorageRecordProcessor.class);
private final Set<E> matchedRecords = new TreeSet<>(this);
private final Set<StorageId> updatedStorageIds = new HashSet<>();
/**
* One type of invalid remote data this handles is two records mapping to the same local data. We
@ -50,6 +53,7 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
if (local.isEmpty()) {
debug(remote.getId(), remote, "[Local Insert] No matching local record. Inserting.");
updatedStorageIds.add(remote.getId());
insertLocal(remote);
return;
}
@ -64,6 +68,7 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
matchedRecords.add(local.get());
final var merged = merge(remote, local.get());
updatedStorageIds.add(merged.getId());
if (!merged.equals(remote)) {
debug(remote.getId(), remote, "[Remote Update] " + merged.describeDiff(remote));
}
@ -75,6 +80,10 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
}
}
public Set<StorageId> getUpdatedStorageIds() {
return Collections.unmodifiableSet(updatedStorageIds);
}
private void debug(StorageId i, E record, String message) {
logger.debug("[{}][{}] {}", i, record.getClass().getSimpleName(), message);
}

View File

@ -21,9 +21,19 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
public class IOUtils {
public static File createTempFile() throws IOException {
final var tempFile = File.createTempFile("signal-cli_tmp_", ".tmp");
tempFile.deleteOnExit();
return tempFile;
final var prefix = "signal-cli_tmp_";
final var suffix = ".tmp";
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
var path = Files.createTempFile(prefix, suffix, PosixFilePermissions.asFileAttribute(perms));
var tempFile = path.toFile();
tempFile.deleteOnExit();
return tempFile;
} catch (UnsupportedOperationException e) {
final var tempFile = File.createTempFile(prefix, suffix);
tempFile.deleteOnExit();
return tempFile;
}
}
public static byte[] readFully(InputStream in) throws IOException {

View File

@ -10,11 +10,11 @@ import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.helper.PinHelper;
import org.signal.core.models.MasterKey;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException;
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.internal.push.LockedException;

View File

@ -7,9 +7,9 @@ import org.signal.libsignal.net.RequestResult;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
import org.signal.network.NetworkResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResult;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.util.StreamDetails;

View File

@ -1 +1 @@
0.90.0
0.94.1

View File

@ -13,7 +13,7 @@ signal-cli-dbus - A commandline and dbus interface for the Signal messenger
== Synopsis
*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system]
*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system]
*dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/_<phonenumber>] org.asamk.Signal.<method> [string:<string argument>] [array:<type>:<array argument>]

View File

@ -13,9 +13,9 @@ signal-cli-jsonrpc - A commandline and dbus interface for the Signal messenger
== Synopsis
*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]]
*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]]
*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] jsonRpc
*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] jsonRpc
== Description

View File

@ -13,7 +13,7 @@ signal-cli - A commandline interface for the Signal messenger
== Synopsis
*signal-cli* [--config CONFIG] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options]
*signal-cli* [--data-dir DATA_DIR] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options]
== Description
@ -57,8 +57,8 @@ If `--verbose` is also given, the detailed logs will only be written to the log
Scrub possibly sensitive information from the log, like phone numbers and UUIDs.
Doesn't work reliably on dbus logs with very verbose logging (`-vvv`)
*--config* CONFIG::
Set the path, where to store the config.
*-d* DATA_DIR, *--data-dir* DATA_DIR, *-c* CONFIG, *--config* CONFIG::
Set the path where to store account data and local configuration.
Make sure you have full read/write access to the given directory.
(Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`))
@ -1186,10 +1186,25 @@ signal-cli -a ACCOUNT trust -a RECIPIENT
== Files
The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*:
The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--data-dir* (legacy *--config*):
`$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`)
=== Configuration file
signal-cli supports a JSON-based global configuration file that provides defaults for CLI options.
Keys use camelCase and generally match the long CLI parameter names (for example `dataDir`, `verbose`, `logFile`, `serviceEnvironment`, `trustNewIdentities`, `output`, `disableSendLog`, `account`).
Configuration files are read and merged in this order; later files override earlier ones:
- `/etc/signal-cli/config.json` (system-wide defaults)
- the path in the `SIGNAL_CLI_CONFIG` environment variable (if set)
- `$XDG_CONFIG_HOME/signal-cli/config.json` (per-user; defaults to `$HOME/.config/signal-cli/config.json`)
When multiple configuration files are present their settings are merged; values from later files override earlier values.
Command-line options always take precedence over configuration file values.
Overall precedence (highest → lowest): command-line options → per-user config → system config → built-in defaults.
== Authors
Maintained by AsamK <asamk@gmx.de>, who is assisted by other open source contributors.

View File

@ -1,13 +1,12 @@
ARG ZULU_TAG="25.0.2-jdk@sha256:9582df6c4415d9c770eb5ff8fce426ebba53631149c9eb083ee126568d32fab3"
ARG ZULU_TAG="25-latest@sha256:8eca9375451a392bff01efe946f2e9263c50aa71a9d68423c068cc1061a41b7e"
FROM docker.io/azul/zulu-openjdk:$ZULU_TAG
ENV SOURCE_DATE_EPOCH=1767225600
ARG SOURCE_DATE_EPOCH="1776889382"
ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH
ENV LANG=C.UTF-8
ENV LC_CTYPE=en_US.UTF-8
ARG SNAPSHOT=20260101T000000Z
RUN echo "deb http://snapshot.ubuntu.com/ubuntu/${SNAPSHOT}/ jammy main" > /etc/apt/sources.list \
&& echo "deb http://snapshot.ubuntu.com/ubuntu/${SNAPSHOT}/ jammy universe" >> /etc/apt/sources.list
RUN apt update && apt install -y make asciidoc-base
RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \
&& sed -i 's/^deb /deb [snapshot=yes] /' /etc/apt/sources.list && apt update --snapshot "$SNAPSHOT" && apt install -y make asciidoc-base --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
WORKDIR /signal-cli
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ]

View File

@ -18,11 +18,8 @@ fi
VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1)
echo "$VERSION" >dist/VERSION
$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:native -f reproducible-builds/native.Containerfile .
$ENGINE build -t signal-cli:client -f reproducible-builds/client.Containerfile .
# Build jar
$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/**'
# shellcheck disable=SC2086
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:build
@ -34,12 +31,14 @@ if [ -n "${OVERRIDE_JAVA_VERSION:-}" ]; then
fi
# Build native-image
$ENGINE build -t signal-cli:native -f reproducible-builds/native.Containerfile .
git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**'
# shellcheck disable=SC2086
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:native
mv build/signal-cli-*-Linux-native.tar.gz dist/
# Build rust client
$ENGINE build -t signal-cli:client -f reproducible-builds/client.Containerfile .
git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**'
# shellcheck disable=SC2086
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:client

View File

@ -1,5 +1,8 @@
FROM docker.io/rust:1.94.1-slim-trixie@sha256:c6a474d7164ea2455e09b60a759b1edca38db7373c5689c1dae31780de4e71ac
ENV SOURCE_DATE_EPOCH=1767225600
ARG RUST_TAG="1-slim@sha256:715efd1ccdc4a63bd6a6e2f54387fff73f904b70e610d41b4d9d74ff38e13ad3"
FROM docker.io/rust:$RUST_TAG
ARG SOURCE_DATE_EPOCH="1776889382"
ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH
ENV LANG=C.UTF-8
ENV LC_CTYPE=en_US.UTF-8
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh

View File

@ -1,5 +1,8 @@
FROM container-registry.oracle.com/graalvm/native-image:25.0.2@sha256:4c0d5919f6840d89721274eb8cf81962faa2f870b816967e6732e2a151b150d8
ENV SOURCE_DATE_EPOCH=1767225600
ARG GRAALVM_TAG="25@sha256:38f835ccb37d4a106c37376a98e8713999077a8c8173d9876505f77da438332c"
FROM container-registry.oracle.com/graalvm/native-image:$GRAALVM_TAG
ARG SOURCE_DATE_EPOCH="1776889382"
ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH
ENV LANG=C.UTF-8
ENV LC_CTYPE=en_US.UTF-8
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh

View File

@ -0,0 +1,57 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../"
cd "$ROOT_DIR"
if command -v podman >/dev/null; then
ENGINE=podman
elif command -v docker >/dev/null; then
ENGINE=docker
else
echo "error: neither podman nor docker is available" >&2
exit 1
fi
resolve_digest() {
local image_ref="$1"
"$ENGINE" pull "$image_ref" >/dev/null
"$ENGINE" image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$image_ref" \
| grep -m1 -E '@sha256:[0-9a-f]{64}$' \
| sed -E 's|.*(@sha256:[0-9a-f]{64})$|\1|'
}
update_arg_tag() {
local file="$1"
local arg_name="$2"
local image_prefix="$3"
local current
current="$(sed -n "s/^ARG ${arg_name}=\"\([^\"]*\)\"$/\\1/p" "$file")"
if [[ -z "$current" ]]; then
echo "error: could not find ARG ${arg_name} in $file" >&2
exit 1
fi
local tag
tag="${current%@*}"
local digest
digest="$(resolve_digest "${image_prefix}${tag}")"
sed -i -E "s|^ARG ${arg_name}=\"[^\"]+\"$|ARG ${arg_name}=\"${tag}${digest}\"|" "$file"
echo "updated $file -> ${tag}${digest}"
}
update_source_date_epoch() {
local file="$1"
local current_timestamp
current_timestamp="$(date +%s)"
sed -i -E "s|^ARG SOURCE_DATE_EPOCH=\"[^\"]+\"$|ARG SOURCE_DATE_EPOCH=\"${current_timestamp}\"|" "$file"
echo "updated $file SOURCE_DATE_EPOCH -> ${current_timestamp}"
}
update_arg_tag reproducible-builds/build.Containerfile ZULU_TAG docker.io/azul/zulu-openjdk:
update_arg_tag reproducible-builds/native.Containerfile GRAALVM_TAG container-registry.oracle.com/graalvm/native-image:
update_arg_tag reproducible-builds/client.Containerfile RUST_TAG docker.io/rust:
update_source_date_epoch reproducible-builds/build.Containerfile
update_source_date_epoch reproducible-builds/native.Containerfile
update_source_date_epoch reproducible-builds/client.Containerfile

View File

@ -38,6 +38,13 @@ else
SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli"
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() {
# To update graalvm config, set GRAALVM_HOME, e.g:
# export GRAALVM_HOME=/usr/lib/jvm/java-25-graalvm
@ -48,11 +55,23 @@ run() {
set -x
if [ "$JSON_RPC" -eq 1 ]; then
"$SIGNAL_CLI" $@
if [ -n "$STD_BUF" ]; then
$STD_BUF "$SIGNAL_CLI" $@
else
"$SIGNAL_CLI" $@
fi
elif [ "$DBUS" -eq 1 ]; then
"$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org'
if [ -n "$STD_BUF" ]; then
$STD_BUF "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org'
else
"$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org'
fi
else
"$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org'
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'
else
"$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org'
fi
fi
set +x
}
@ -98,9 +117,10 @@ link() {
rm -f "$LINK_CODE_FILE"
mkfifo "$LINK_CODE_FILE"
run_linked link -n "test-device" >"$LINK_CODE_FILE" &
read LINK_CODE <"$LINK_CODE_FILE"
LINK_PID=$!
read -r LINK_CODE <"$LINK_CODE_FILE"
run_main -a "$NUMBER" addDevice --uri "$LINK_CODE"
wait
wait $LINK_PID
run_linked -a "$NUMBER" send --note-to-self -m hi
run_main -a "$NUMBER" receive
run_linked -a "$NUMBER" receive
@ -180,6 +200,7 @@ 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" unblock "$NUMBER_1"
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_2" receive
@ -207,6 +228,7 @@ run_main -a "$NUMBER_1" updateGroup -g "$GROUP_ID" -m "$NUMBER_2"
run_main -a "$NUMBER_1" listGroups -d
run_main -a "$NUMBER_1" --output=json listGroups -d
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" listGroups -d
run_main -a "$NUMBER_2" --output=json listGroups -d
@ -228,6 +250,7 @@ for OUTPUT in "plain-text" "json"; do
run_main -a "$NUMBER_2" --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 "$NUMBER_2" -m test
run_main -a "$NUMBER_2" --output="$OUTPUT" receive
done
@ -235,8 +258,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=="
## Provisioning
link "$NUMBER_1"
link "$NUMBER_2"
link "$NUMBER_1" || true
link "$NUMBER_2" || true
run_main -a "$NUMBER_1" listDevices
run_linked -a "$NUMBER_1" sendSyncRequest
run_main -a "$NUMBER_1" sendContacts

View File

@ -46,7 +46,9 @@ public class App {
private final Namespace ns;
static ArgumentParser buildArgumentParser() {
static ArgumentParser buildArgumentParser(GlobalConfig config) {
final var cfg = config == null ? GlobalConfig.DEFAULT : config;
var parser = ArgumentParsers.newFor("signal-cli", VERSION_0_9_0_DEFAULT_SETTINGS)
.includeArgumentNamesAsKeysInResult(true)
.build()
@ -57,47 +59,60 @@ public class App {
parser.addArgument("--version").help("Show package version.").action(Arguments.version());
parser.addArgument("-v", "--verbose")
.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());
parser.addArgument("--log-file")
.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.")
.setDefault(cfg.logFile() == null ? null : new File(cfg.logFile()));
parser.addArgument("--scrub-log")
.action(Arguments.storeTrue())
.help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.");
parser.addArgument("-c", "--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
.help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.")
.setDefault(cfg.scrubLog() == null ? false : cfg.scrubLog());
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).")
.setDefault(cfg.dataDir());
parser.addArgument("-a", "--account", "-u", "--username")
.help("Specify your phone number, that will be your identifier.");
var mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("--dbus").dest("global-dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
mut.addArgument("--dbus")
.dest("global-dbus")
.help("Make request via user dbus.")
.action(Arguments.storeTrue())
.setDefault(cfg.dbus() == null ? false : cfg.dbus());
mut.addArgument("--dbus-system")
.dest("global-dbus-system")
.help("Make request via system dbus.")
.action(Arguments.storeTrue());
.action(Arguments.storeTrue())
.setDefault(cfg.dbusSystem() == null ? false : cfg.dbusSystem());
parser.addArgument("--bus-name")
.dest("global-bus-name")
.setDefault(DbusConfig.getBusname())
.setDefault(cfg.busName() != null ? cfg.busName() : DbusConfig.getBusname())
.help("Specify the D-Bus bus name to connect to.");
parser.addArgument("-o", "--output")
.help("Choose to output in plain text or JSON")
.type(Arguments.enumStringType(OutputType.class));
.type(Arguments.enumStringType(OutputType.class))
.setDefault(cfg.output() == null ? null : cfg.output());
parser.addArgument("--service-environment")
.help("Choose the server environment to use.")
.type(Arguments.enumStringType(ServiceEnvironmentCli.class))
.setDefault(ServiceEnvironmentCli.LIVE);
.setDefault(cfg.serviceEnvironment() != null ? cfg.serviceEnvironment() : ServiceEnvironmentCli.LIVE);
parser.addArgument("--trust-new-identities")
.help("Choose when to trust new identities.")
.type(Arguments.enumStringType(TrustNewIdentityCli.class))
.setDefault(TrustNewIdentityCli.ON_FIRST_USE);
.setDefault(cfg.trustNewIdentities() != null
? cfg.trustNewIdentities()
: TrustNewIdentityCli.ON_FIRST_USE);
parser.addArgument("--disable-send-log")
.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);
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.");
@ -219,12 +234,12 @@ public class App {
}
private SignalAccountFiles loadSignalAccountFiles() throws IOErrorException {
final File configPath;
final var config = ns.getString("config");
if (config != null) {
configPath = new File(config);
final File dataPath;
final var dataDir = ns.getString("data-dir");
if (dataDir != null) {
dataPath = new File(dataDir);
} else {
configPath = getDefaultConfigPath();
dataPath = getDefaultDataPath();
}
final var serviceEnvironmentCli = ns.<ServiceEnvironmentCli>get("service-environment");
@ -240,7 +255,7 @@ public class App {
final var disableSendLog = Boolean.TRUE.equals(ns.getBoolean("disable-send-log"));
try {
return new SignalAccountFiles(configPath,
return new SignalAccountFiles(dataPath,
serviceEnvironment,
BaseConfig.USER_AGENT,
new Settings(trustNewIdentity, disableSendLog));
@ -339,7 +354,7 @@ public class App {
/**
* @return the default data directory to be used by signal-cli.
*/
private static File getDefaultConfigPath() {
private static File getDefaultDataPath() {
return new File(IOUtils.getDataHomeDir(), "signal-cli");
}
}

View File

@ -8,7 +8,7 @@ public class BaseConfig {
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"))
.orElse("Signal-Android/8.6.1");
.orElse("Signal-Android/8.12.1");
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
? "signal-cli"
: PROJECT_NAME + "/" + PROJECT_VERSION;

View File

@ -0,0 +1,81 @@
package org.asamk.signal;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.commands.exceptions.UserErrorException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Loads and merges configuration files. Merge order (later files override earlier):
* - /etc/signal-cli/config.json
* - file pointed to by SIGNAL_CLI_CONFIG (if set)
* - $XDG_CONFIG_HOME/signal-cli/config.json or $HOME/.config/signal-cli/config.json
*/
public final class ConfigLoader {
private ConfigLoader() {
}
public static GlobalConfig load() throws UserErrorException {
final ObjectMapper mapper = new ObjectMapper();
final ObjectNode merged = mapper.createObjectNode();
// System config
addIfExists(merged, mapper, Paths.get("/etc/signal-cli/config.json"));
// User config via env (if set) else XDG or ~/.config
final String env = System.getenv("SIGNAL_CLI_CONFIG");
if (env != null && !env.isEmpty()) {
addIfExists(merged, mapper, Paths.get(env));
} else {
final String xdg = System.getenv("XDG_CONFIG_HOME");
if (xdg != null && !xdg.isEmpty()) {
addIfExists(merged, mapper, Paths.get(xdg, "signal-cli", "config.json"));
} else {
addIfExists(merged,
mapper,
Paths.get(System.getProperty("user.home"), ".config", "signal-cli", "config.json"));
}
}
try {
if (merged.isEmpty()) {
return GlobalConfig.DEFAULT;
}
return mapper.treeToValue(merged, GlobalConfig.class);
} catch (Exception e) {
throw new UserErrorException("Failed to parse configuration file(s): " + e.getMessage(), e);
}
}
private static void addIfExists(ObjectNode merged, ObjectMapper mapper, Path p) throws UserErrorException {
if (p == null) return;
try {
if (Files.exists(p)) {
final JsonNode node = mapper.readTree(p.toFile());
merge(merged, node);
}
} catch (IOException e) {
throw new UserErrorException("Failed to load config from " + p + ": " + e.getMessage(), e);
}
}
private static void merge(ObjectNode target, JsonNode source) {
source.properties().forEach(entry -> {
final String name = entry.getKey();
final JsonNode value = entry.getValue();
final JsonNode existing = target.get(name);
if (existing != null && existing.isObject() && value.isObject()) {
merge((ObjectNode) existing, value);
} else {
target.set(name, value);
}
});
}
}

View File

@ -0,0 +1,36 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonProperty;
public record GlobalConfig(
@JsonProperty("verbose") Integer verbose,
@JsonProperty("logFile") String logFile,
@JsonProperty("scrubLog") Boolean scrubLog,
@JsonProperty("dataDir") String dataDir,
@JsonProperty("busName") String busName,
@JsonProperty("dbus") Boolean dbus,
@JsonProperty("dbusSystem") Boolean dbusSystem,
@JsonProperty("output") OutputType output,
@JsonProperty("serviceEnvironment") ServiceEnvironmentCli serviceEnvironment,
@JsonProperty("trustNewIdentities") TrustNewIdentityCli trustNewIdentities,
@JsonProperty("disableSendLog") Boolean disableSendLog,
@JsonProperty("account") String account
) {
public static final GlobalConfig DEFAULT = new GlobalConfig(null,
null,
null,
null,
null,
null,
null,
null,
ServiceEnvironmentCli.LIVE,
TrustNewIdentityCli.ON_FIRST_USE,
null,
null);
public static GlobalConfig empty() {
return new GlobalConfig(null, null, null, null, null, null, null, null, null, null, null, null);
}
}

View File

@ -40,27 +40,32 @@ import java.security.Security;
public class Main {
public static void main(String[] args) {
static void main(String[] args) {
// enable unlimited strength crypto via Policy, supported on relevant JREs
Security.setProperty("crypto.policy", "unlimited");
installSecurityProviderWorkaround();
// Load global config early so we can use its values as parser defaults
final GlobalConfig globalConfig;
try {
globalConfig = ConfigLoader.load();
} catch (UserErrorException e) {
System.exit(handleCommandException(e, null));
return;
}
// Configuring the logger needs to happen before any logger is initialized
final var loggingConfig = parseLoggingConfig(args);
final var loggingConfig = parseLoggingConfig(args, globalConfig);
configureLogging(loggingConfig);
final var parser = App.buildArgumentParser();
final var parser = App.buildArgumentParser(globalConfig);
final var ns = parser.parseArgsOrFail(args);
int status = 0;
try {
new App(ns).init();
} catch (CommandException e) {
System.err.println(e.getMessage());
if (loggingConfig.verboseLevel > 0 && e.getCause() != null) {
e.getCause().printStackTrace(System.err);
}
status = getStatusForError(e);
status = handleCommandException(e, loggingConfig);
} catch (Throwable e) {
e.printStackTrace(System.err);
status = 2;
@ -69,16 +74,27 @@ public class Main {
System.exit(status);
}
private static int handleCommandException(final CommandException e, final LoggingConfig loggingConfig) {
System.err.println(e.getMessage());
if (loggingConfig != null && loggingConfig.verboseLevel > 0 && e.getCause() != null) {
e.getCause().printStackTrace(System.err);
}
return getStatusForError(e);
}
private static void installSecurityProviderWorkaround() {
// Register our own security provider
Security.insertProviderAt(new SecurityProvider(), 1);
Security.addProvider(new BouncyCastleProvider());
}
private static LoggingConfig parseLoggingConfig(final String[] args) {
final var nsLog = parseArgs(args);
private static LoggingConfig parseLoggingConfig(final String[] args, final GlobalConfig config) {
final var nsLog = parseArgs(args, config);
if (nsLog == null) {
return new LoggingConfig(0, null, false);
final var verbose = config != null && config.verbose() != null ? config.verbose() : 0;
final var logFile = config != null && config.logFile() != null ? new File(config.logFile()) : null;
final var scrubLog = config != null && Boolean.TRUE.equals(config.scrubLog());
return new LoggingConfig(verbose, logFile, scrubLog);
}
final var verboseLevel = nsLog.getInt("verbose");
@ -90,14 +106,20 @@ public class Main {
/**
* This method only parses commandline args relevant for logging configuration.
*/
private static Namespace parseArgs(String[] args) {
private static Namespace parseArgs(String[] args, final GlobalConfig config) {
var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS)
.includeArgumentNamesAsKeysInResult(true)
.build()
.defaultHelp(false);
parser.addArgument("-v", "--verbose").action(Arguments.count());
parser.addArgument("--log-file").type(File.class);
parser.addArgument("--scrub-log").action(Arguments.storeTrue());
parser.addArgument("-v", "--verbose")
.action(Arguments.count())
.setDefault(config == null || config.verbose() == null ? 0 : config.verbose());
parser.addArgument("--log-file")
.type(File.class)
.setDefault(config == null || config.logFile() == null ? null : new File(config.logFile()));
parser.addArgument("--scrub-log")
.action(Arguments.storeTrue())
.setDefault(config == null || config.scrubLog() == null ? false : config.scrubLog());
try {
return parser.parseKnownArgs(args, null);
@ -124,12 +146,12 @@ public class Main {
private static int getStatusForError(final CommandException e) {
return switch (e) {
case UserErrorException userErrorException -> 1;
case UnexpectedErrorException unexpectedErrorException -> 2;
case IOErrorException ioErrorException -> 3;
case UntrustedKeyErrorException untrustedKeyErrorException -> 4;
case RateLimitErrorException rateLimitErrorException -> 5;
case CaptchaRejectedErrorException captchaRejectedErrorException -> 6;
case UserErrorException _ -> 1;
case UnexpectedErrorException _ -> 2;
case IOErrorException _ -> 3;
case UntrustedKeyErrorException _ -> 4;
case RateLimitErrorException _ -> 5;
case CaptchaRejectedErrorException _ -> 6;
case null -> 2;
};
}

View File

@ -1,5 +1,7 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonCreator;
public enum OutputType {
PLAIN_TEXT {
@Override
@ -12,5 +14,16 @@ public enum OutputType {
public String toString() {
return "json";
}
},
};
@JsonCreator
public static OutputType fromString(String value) {
if (value == null) return null;
final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", "");
return switch (norm) {
case "plaintext" -> PLAIN_TEXT;
case "json" -> JSON;
default -> throw new IllegalArgumentException("Invalid output type: " + value);
};
}
}

View File

@ -1,5 +1,7 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonCreator;
public enum ServiceEnvironmentCli {
LIVE {
@Override
@ -12,5 +14,16 @@ public enum ServiceEnvironmentCli {
public String toString() {
return "staging";
}
},
};
@JsonCreator
public static ServiceEnvironmentCli fromString(String value) {
if (value == null) return null;
final var norm = value.trim().toLowerCase();
return switch (norm) {
case "live" -> LIVE;
case "staging" -> STAGING;
default -> throw new IllegalArgumentException("Invalid service-environment: " + value);
};
}
}

View File

@ -1,5 +1,7 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonCreator;
public enum TrustNewIdentityCli {
ALWAYS {
@Override
@ -18,5 +20,17 @@ public enum TrustNewIdentityCli {
public String toString() {
return "never";
}
},
};
@JsonCreator
public static TrustNewIdentityCli fromString(String value) {
if (value == null) return null;
final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", "");
return switch (norm) {
case "always" -> ALWAYS;
case "onfirstuse" -> ON_FIRST_USE;
case "never" -> NEVER;
default -> throw new IllegalArgumentException("Invalid trust-new-identities: " + value);
};
}
}

View File

@ -144,8 +144,7 @@ public class SendCommand implements JsonRpcLocalCommand {
}
try {
final var results = m.sendEndSessionMessage(singleRecipients);
outputResult(outputWriter, results);
m.sendEndSessionMessage(singleRecipients);
return;
} catch (IOException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()

View File

@ -549,9 +549,8 @@ public class DbusManagerImpl implements Manager {
}
@Override
public SendMessageResults 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());
return new SendMessageResults(0, Map.of());
}
@Override

View File

@ -430,8 +430,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
@Override
public void sendEndSessionMessage(final List<String> recipients) {
try {
final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber()));
checkSendMessageResults(results);
m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber()));
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}

View File

@ -19,8 +19,11 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
@ -37,12 +40,14 @@ public class HttpServerHandler implements AutoCloseable {
private final Manager m;
private HttpServer server;
private final AtomicBoolean shutdown = new AtomicBoolean(false);
private final Set<String> allowedHosts;
public HttpServerHandler(final InetSocketAddress address, final Manager m) {
this.address = address;
commandHandler = new SignalJsonRpcCommandHandler(m, Commands::getCommand);
this.c = null;
this.m = m;
this.allowedHosts = buildAllowedHosts(address);
}
public HttpServerHandler(final InetSocketAddress address, final MultiAccountManager c) {
@ -50,6 +55,7 @@ public class HttpServerHandler implements AutoCloseable {
commandHandler = new SignalJsonRpcCommandHandler(c, Commands::getCommand);
this.c = c;
this.m = null;
this.allowedHosts = buildAllowedHosts(address);
}
public void init() throws IOException {
@ -67,6 +73,7 @@ public class HttpServerHandler implements AutoCloseable {
server.start();
logger.info("Started HTTP server on {}", address);
logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts);
}
@Override
@ -99,6 +106,12 @@ public class HttpServerHandler implements AutoCloseable {
}
private void handleRpcEndpoint(HttpExchange httpExchange) throws IOException {
if (!isHostAllowed(httpExchange)) {
logger.warn("Rejected RPC request with invalid Host header: {} from {}",
httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress());
sendResponse(421, null, httpExchange);
return;
}
if (!"/api/v1/rpc".equals(httpExchange.getRequestURI().getPath())) {
sendResponse(404, null, httpExchange);
return;
@ -146,6 +159,12 @@ public class HttpServerHandler implements AutoCloseable {
}
private void handleEventsEndpoint(HttpExchange httpExchange) throws IOException {
if (!isHostAllowed(httpExchange)) {
logger.warn("Rejected Events request with invalid Host header: {} from {}",
httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress());
sendResponse(421, null, httpExchange);
return;
}
if (!"/api/v1/events".equals(httpExchange.getRequestURI().getPath())) {
sendResponse(404, null, httpExchange);
return;
@ -169,6 +188,12 @@ public class HttpServerHandler implements AutoCloseable {
httpExchange.sendResponseHeaders(200, 0);
final var sender = new ServerSentEventSender(httpExchange.getResponseBody());
// Flush HTTP response headers to the client immediately.
// Without this, the JVM HttpServer buffers everything until a later write
// in the keep-alive loop (15 s), causing clients with shorter timeouts
// (e.g. 10 s) to abort before receiving the initial response.
httpExchange.getResponseBody().flush();
final var shouldStop = new AtomicBoolean(false);
final var handlers = subscribeReceiveHandlers(managers, sender, () -> {
shouldStop.set(true);
@ -267,4 +292,59 @@ public class HttpServerHandler implements AutoCloseable {
void call();
}
private Set<String> buildAllowedHosts(final InetSocketAddress address) {
final var s = new HashSet<String>();
final var host = address == null ? null : address.getHostString();
if (host != null && !host.isEmpty()) {
s.add(host.toLowerCase(Locale.ROOT));
}
s.add("localhost");
s.add("127.0.0.1");
s.add("::1");
return s;
}
private boolean isHostAllowed(final HttpExchange httpExchange) {
final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host");
if (hostHeader == null || hostHeader.isEmpty()) {
return false;
}
String hostPart = hostHeader;
String portPart = null;
if (hostHeader.startsWith("[")) {
final var idx = hostHeader.indexOf(']');
if (idx == -1) return false;
hostPart = hostHeader.substring(1, idx);
if (hostHeader.length() > idx + 1 && hostHeader.charAt(idx + 1) == ':') {
portPart = hostHeader.substring(idx + 2);
}
} else {
final var colon = hostHeader.lastIndexOf(':');
if (colon != -1) {
final var possiblePort = hostHeader.substring(colon + 1);
if (possiblePort.chars().allMatch(Character::isDigit)) {
hostPart = hostHeader.substring(0, colon);
portPart = possiblePort;
}
}
}
hostPart = hostPart.toLowerCase(Locale.ROOT);
if (!allowedHosts.contains(hostPart)) {
return false;
}
if (portPart != null) {
try {
final var port = Integer.parseInt(portPart);
if (port != address.getPort()) return false;
} catch (NumberFormatException e) {
return false;
}
}
return true;
}
}

View File

@ -25,12 +25,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class SignalJsonRpcDispatcherHandler {
@ -41,8 +43,8 @@ public class SignalJsonRpcDispatcherHandler {
private final JsonRpcReader jsonRpcReader;
private final boolean noReceiveOnStart;
private final Map<Integer, List<Pair<Manager, Manager.ReceiveMessageHandler>>> receiveHandlers = new HashMap<>();
private final Map<Integer, List<Pair<Manager, Manager.CallEventListener>>> callEventHandlers = new HashMap<>();
private final Map<Integer, ArrayList<Pair<Manager, Manager.ReceiveMessageHandler>>> receiveHandlers = new HashMap<>();
private final Map<Integer, ArrayList<Pair<Manager, Manager.CallEventListener>>> callEventHandlers = new HashMap<>();
private SignalJsonRpcCommandHandler commandHandler;
public SignalJsonRpcDispatcherHandler(
@ -91,7 +93,9 @@ public class SignalJsonRpcDispatcherHandler {
private int subscribeCallEvents(final Collection<Manager> managers) {
final var subscriptionId = nextSubscriptionId.getAndIncrement();
final var listeners = managers.stream().map(m -> createCallEventHandler(m, subscriptionId)).toList();
final var listeners = managers.stream()
.map(m -> createCallEventHandler(m, subscriptionId))
.collect(Collectors.toCollection(ArrayList::new));
callEventHandlers.put(subscriptionId, listeners);
return subscriptionId;
}
@ -146,7 +150,7 @@ public class SignalJsonRpcDispatcherHandler {
final var subscriptionId = nextSubscriptionId.getAndIncrement();
final var handlers = managers.stream()
.map(m -> createReceiveHandler(m, subscriptionId, internalSubscription))
.toList();
.collect(Collectors.toCollection(ArrayList::new));
receiveHandlers.put(subscriptionId, handlers);
return subscriptionId;

View File

@ -1317,6 +1317,14 @@
}
]
},
{
"type": "java.util.concurrent.CopyOnWriteArrayList",
"fields": [
{
"name": "lock"
}
]
},
{
"type": "java.util.concurrent.ForkJoinTask",
"fields": [
@ -1564,6 +1572,9 @@
}
]
},
{
"type": "kotlin.reflect.jvm.internal.impl.km.jvm.internal.JvmMetadataExtensions"
},
{
"type": "kotlin.reflect.jvm.internal.impl.load.java.ErasedOverridabilityCondition"
},
@ -1575,42 +1586,13 @@
},
{
"type": "kotlin.reflect.jvm.internal.impl.resolve.scopes.DescriptorKindFilter",
"allPublicFields": true,
"fields": [
{
"name": "ALL"
},
{
"name": "CALLABLES"
},
{
"name": "CLASSIFIERS"
},
{
"name": "Companion"
},
{
"name": "FUNCTIONS"
},
{
"name": "NON_SINGLETON_CLASSIFIERS"
},
{
"name": "PACKAGES"
},
{
"name": "SINGLETON_CLASSIFIERS"
},
{
"name": "TYPE_ALIASES"
},
{
"name": "VALUES"
},
{
"name": "VARIABLES"
}
]
"allPublicFields": true
},
{
"type": "kotlinx.coroutines.AwaitAll"
},
{
"type": "kotlinx.coroutines.AwaitAll$AwaitAllNode"
},
{
"type": "kotlinx.coroutines.CancellableContinuationImpl",
@ -1626,6 +1608,9 @@
}
]
},
{
"type": "kotlinx.coroutines.CancelledContinuation"
},
{
"type": "kotlinx.coroutines.CompletedExceptionally",
"fields": [
@ -1681,6 +1666,9 @@
}
]
},
{
"type": "kotlinx.coroutines.internal.LimitedDispatcher"
},
{
"type": "kotlinx.coroutines.internal.LockFreeLinkedListNode",
"fields": [
@ -1714,6 +1702,9 @@
}
]
},
{
"type": "kotlinx.coroutines.internal.ThreadSafeHeap"
},
{
"type": "kotlinx.coroutines.scheduling.CoroutineScheduler",
"fields": [
@ -1728,6 +1719,12 @@
}
]
},
{
"type": "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
},
{
"type": "kotlinx.coroutines.scheduling.WorkQueue"
},
{
"type": "libcore.io.Memory"
},
@ -1961,6 +1958,28 @@
{
"type": "org.asamk.SignalControl.Error.InvalidNumber"
},
{
"type": "org.asamk.signal.GlobalConfig",
"methods": [
{
"name": "<init>",
"parameterTypes": [
"java.lang.Integer",
"java.lang.String",
"java.lang.Boolean",
"java.lang.String",
"java.lang.String",
"java.lang.Boolean",
"java.lang.Boolean",
"org.asamk.signal.OutputType",
"org.asamk.signal.ServiceEnvironmentCli",
"org.asamk.signal.TrustNewIdentityCli",
"java.lang.Boolean",
"java.lang.String"
]
}
]
},
{
"type": "org.asamk.signal.Main",
"jniAccessible": true,
@ -1977,6 +1996,31 @@
}
]
},
{
"type": "org.asamk.signal.OutputType"
},
{
"type": "org.asamk.signal.ServiceEnvironmentCli",
"methods": [
{
"name": "fromString",
"parameterTypes": [
"java.lang.String"
]
}
]
},
{
"type": "org.asamk.signal.TrustNewIdentityCli",
"methods": [
{
"name": "fromString",
"parameterTypes": [
"java.lang.String"
]
}
]
},
{
"type": "org.asamk.signal.commands.AcceptCallCommand$JsonCallInfo",
"allDeclaredFields": true,
@ -4898,6 +4942,15 @@
}
]
},
{
"type": "org.bouncycastle.jcajce.provider.kdf.PBKDF2$Mappings",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
},
{
"type": "org.bouncycastle.jcajce.provider.kdf.SCRYPT$Mappings",
"methods": [
@ -6461,6 +6514,68 @@
"type": "org.signal.libsignal.zkgroup.profiles.ProfileKey",
"allDeclaredFields": true
},
{
"type": "org.signal.network.api.SenderCertificate",
"methods": [
{
"name": "<init>",
"parameterTypes": []
},
{
"name": "setCertificate",
"parameterTypes": [
"byte[]"
]
}
]
},
{
"type": "org.signal.network.api.SenderCertificate$ByteArrayDeserializer",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
},
{
"type": "org.signal.network.api.SenderCertificate$ByteArraySerializer"
},
{
"type": "org.signal.network.api.SubmitRecaptchaChallengePayload",
"methods": [
{
"name": "getCaptcha",
"parameterTypes": []
},
{
"name": "getToken",
"parameterTypes": []
},
{
"name": "getType",
"parameterTypes": []
}
]
},
{
"type": "org.signal.network.util.JsonUtil$IdentityKeyDeserializer",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
},
{
"type": "org.signal.network.util.JsonUtil$ServiceIdDeserializer",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
},
{
"type": "org.signal.storageservice.storage.protos.groups.AccessControl",
"fields": [
@ -10115,6 +10230,9 @@
{
"glob": "META-INF/services/java.util.spi.ResourceBundleControlProvider"
},
{
"glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.km.internal.extensions.MetadataExtensions"
},
{
"glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition"
},

View File

@ -0,0 +1,536 @@
package org.asamk.signal.http;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.CallInfo;
import org.asamk.signal.manager.api.CallOffer;
import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.GroupId;
import org.asamk.signal.manager.api.GroupInviteLinkUrl;
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.IdentityVerificationCode;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ReceiveConfig;
import org.asamk.signal.manager.api.Recipient;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResult;
import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.StickerPack;
import org.asamk.signal.manager.api.StickerPackId;
import org.asamk.signal.manager.api.StickerPackUrl;
import org.asamk.signal.manager.api.TurnServer;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl;
import org.asamk.signal.manager.api.UsernameStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.URI;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Regression test for the SSE initial-flush bug:
* HttpServerHandler used to flush the initial SSE response only after a later
* write in the 15-second keep-alive loop, meaning the HTTP response headers
* were not flushed to the client until then.
* Clients with a shorter connection timeout (e.g. 10 s) would time out before
* receiving the initial response.
*
* This test verifies that the endpoint returns HTTP 200 within 2 seconds of
* connecting to GET /api/v1/events.
*/
class SseInitialFlushTest {
private HttpServerHandler handler;
private int port;
/** Finds a free local port. */
private static int freePort() throws Exception {
try (var ss = new ServerSocket(0)) {
ss.setReuseAddress(true);
return ss.getLocalPort();
}
}
@BeforeEach
void setUp() throws Exception {
port = freePort();
handler = new HttpServerHandler(new InetSocketAddress("127.0.0.1", port), new MinimalStubManager());
handler.init();
}
@AfterEach
void tearDown() {
if (handler != null) {
handler.close();
}
}
/**
* The SSE endpoint MUST flush the initial HTTP response immediately upon
* connection, before the 15-second keep-alive loop fires. A read timeout of
* 2 000 ms is used well below the 15-second wait interval but generous
* enough to survive any CI scheduling jitter.
*/
@Test
void sseEndpointReturnsHeadersWithinTwoSeconds() {
assertDoesNotThrow(() -> {
var url = new URI("http", null, "127.0.0.1", port, "/api/v1/events", null, null).toURL();
var conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Accept", "text/event-stream");
conn.setReadTimeout(2_000); // 2 s fails before fix (15 s flush), passes after
conn.setConnectTimeout(2_000);
try {
conn.connect();
assertEquals(200, conn.getResponseCode());
} finally {
conn.disconnect();
}
}, "SSE endpoint did not return the initial response within 2 seconds");
}
// -------------------------------------------------------------------------
// Minimal Manager stub only receive-handler methods need real behaviour;
// everything else is a no-op stub.
// -------------------------------------------------------------------------
private static final class MinimalStubManager implements Manager {
@Override
public String getSelfNumber() {
return "+10000000000";
}
@Override
public void addReceiveHandler(ReceiveMessageHandler handler, boolean isWeakListener) {
// no-op
}
@Override
public void removeReceiveHandler(ReceiveMessageHandler handler) {
// no-op
}
@Override
public boolean isReceiving() {
return false;
}
@Override
public void receiveMessages(Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler) {
}
@Override
public void stopReceiveMessages() {
}
@Override
public void setReceiveConfig(ReceiveConfig receiveConfig) {
}
@Override
public Map<String, UserStatus> getUserStatus(Set<String> numbers) {
return Map.of();
}
@Override
public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) {
return Map.of();
}
@Override
public void updateAccountAttributes(String deviceName, Boolean unidentifiedDeliveryIndicators, Boolean discoverableByNumber, Boolean numberSharing) {
}
@Override
public Configuration getConfiguration() {
return null;
}
@Override
public void updateConfiguration(Configuration configuration) {
}
@Override
public void updateProfile(UpdateProfile updateProfile) {
}
@Override
public String getUsername() {
return null;
}
@Override
public UsernameLinkUrl getUsernameLink() {
return null;
}
@Override
public void setUsername(String username) {
}
@Override
public void deleteUsername() {
}
@Override
public void startChangeNumber(String newNumber, boolean voiceVerification, String captcha) {
}
@Override
public void finishChangeNumber(String newNumber, String verificationCode, String pin) {
}
@Override
public void unregister() {
}
@Override
public void deleteAccount() {
}
@Override
public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) {
}
@Override
public List<Device> getLinkedDevices() {
return List.of();
}
@Override
public void updateLinkedDevice(int deviceId, String name) {
}
@Override
public void removeLinkedDevices(int deviceId) {
}
@Override
public void addDeviceLink(DeviceLinkUrl deviceLinkUrl) {
}
@Override
public void setRegistrationLockPin(Optional<String> pin) {
}
@Override
public List<Group> getGroups() {
return List.of();
}
@Override
public List<Group> getGroups(Collection<GroupId> groupIds) {
return List.of();
}
@Override
public SendGroupMessageResults quitGroup(GroupId groupId, Set<RecipientIdentifier.Single> administrators) {
return null;
}
@Override
public void deleteGroup(GroupId groupId) {
}
@Override
public Pair<GroupId, SendGroupMessageResults> createGroup(String name, Set<RecipientIdentifier.Single> members, String avatarFile) {
return null;
}
@Override
public SendGroupMessageResults updateGroup(GroupId groupId, UpdateGroup updateGroup) {
return null;
}
@Override
public Pair<GroupId, SendGroupMessageResults> joinGroup(GroupInviteLinkUrl inviteLinkUrl) {
return null;
}
@Override
public SendMessageResults sendTypingMessage(TypingAction action, Set<RecipientIdentifier> recipients) {
return null;
}
@Override
public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
return null;
}
@Override
public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
return null;
}
@Override
public SendMessageResults sendMessage(Message message, Set<RecipientIdentifier> recipients, boolean notifySelf) {
return null;
}
@Override
public SendMessageResults sendEditMessage(Message message, Set<RecipientIdentifier> recipients, long targetSentTimestamp) {
return null;
}
@Override
public SendMessageResults sendRemoteDeleteMessage(long targetSentTimestamp, Set<RecipientIdentifier> recipients) {
return null;
}
@Override
public SendMessageResults sendMessageReaction(String emoji, boolean remove, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set<RecipientIdentifier> recipients, boolean notifySelf, boolean story) {
return null;
}
@Override
public SendMessageResults sendAdminDelete(RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set<RecipientIdentifier.Group> recipients, boolean notifySelf, boolean story) {
return null;
}
@Override
public SendMessageResults sendPinMessage(int duration, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set<RecipientIdentifier> recipients, boolean notifySelf, boolean story) {
return null;
}
@Override
public SendMessageResults sendUnpinMessage(RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set<RecipientIdentifier> recipients, boolean notifySelf, boolean story) {
return null;
}
@Override
public SendMessageResults sendPaymentNotificationMessage(byte[] receipt, String note, RecipientIdentifier.Single recipient) {
return null;
}
@Override
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) {
}
@Override
public SendMessageResults sendMessageRequestResponse(MessageEnvelope.Sync.MessageRequestResponse.Type type, Set<RecipientIdentifier> recipients) {
return null;
}
@Override
public SendMessageResults sendPollCreateMessage(String question, boolean multipleChoice, List<String> options, Set<RecipientIdentifier> recipients, boolean notifySelf) {
return null;
}
@Override
public SendMessageResults sendPollVoteMessage(RecipientIdentifier.Single author, long timestamp, List<Integer> optionIds, int version, Set<RecipientIdentifier> recipients, boolean notifySelf) {
return null;
}
@Override
public SendMessageResults sendPollTerminateMessage(long timestamp, Set<RecipientIdentifier> recipients, boolean notifySelf) {
return null;
}
@Override
public void hideRecipient(RecipientIdentifier.Single recipient) {
}
@Override
public void deleteRecipient(RecipientIdentifier.Single recipient) {
}
@Override
public void deleteContact(RecipientIdentifier.Single recipient) {
}
@Override
public void setContactName(RecipientIdentifier.Single recipient, String givenName, String familyName, String newGivenName, String newFamilyName, String nick) {
}
@Override
public void setContactsBlocked(Collection<RecipientIdentifier.Single> recipients, boolean blocked) {
}
@Override
public void setGroupsBlocked(Collection<GroupId> groupIds, boolean blocked) {
}
@Override
public void setExpirationTimer(RecipientIdentifier.Single recipient, int messageExpirationTimer) {
}
@Override
public StickerPackUrl uploadStickerPack(File path) {
return null;
}
@Override
public void installStickerPack(StickerPackUrl url) {
}
@Override
public List<StickerPack> getStickerPacks() {
return List.of();
}
@Override
public void requestAllSyncData() {
}
@Override
public boolean isContactBlocked(RecipientIdentifier.Single recipient) {
return false;
}
@Override
public void sendContacts() {
}
@Override
public List<Recipient> getRecipients(boolean onlyWithProfile, Optional<Boolean> blocked, Collection<RecipientIdentifier.Single> addresses, Optional<String> name) {
return List.of();
}
@Override
public String getContactOrProfileName(RecipientIdentifier.Single recipient) {
return null;
}
@Override
public Group getGroup(GroupId groupId) {
return null;
}
@Override
public List<Identity> getIdentities() {
return List.of();
}
@Override
public List<Identity> getIdentities(RecipientIdentifier.Single recipient) {
return List.of();
}
@Override
public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode) {
return false;
}
@Override
public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) {
return false;
}
@Override
public void addAddressChangedListener(Runnable listener) {
}
@Override
public void addClosedListener(Runnable listener) {
}
@Override
public InputStream retrieveAttachment(String id) {
return null;
}
@Override
public InputStream retrieveContactAvatar(RecipientIdentifier.Single recipient) {
return null;
}
@Override
public InputStream retrieveProfileAvatar(RecipientIdentifier.Single recipient) {
return null;
}
@Override
public InputStream retrieveGroupAvatar(GroupId groupId) {
return null;
}
@Override
public InputStream retrieveSticker(StickerPackId stickerPackId, int stickerId) {
return null;
}
@Override
public CallInfo startCall(RecipientIdentifier.Single recipient) {
return null;
}
@Override
public CallInfo acceptCall(long callId) {
return null;
}
@Override
public void hangupCall(long callId) {
}
@Override
public SendMessageResult rejectCall(long callId) {
return null;
}
@Override
public List<CallInfo> listActiveCalls() {
return List.of();
}
@Override
public void sendCallOffer(RecipientIdentifier.Single recipient, CallOffer callOffer) {
}
@Override
public void sendCallAnswer(RecipientIdentifier.Single recipient, long callId, byte[] answer) {
}
@Override
public void sendIceUpdate(RecipientIdentifier.Single recipient, long callId, List<byte[]> iceCandidates) {
}
@Override
public void sendHangup(RecipientIdentifier.Single recipient, long callId, MessageEnvelope.Call.Hangup.Type type) {
}
@Override
public void sendBusy(RecipientIdentifier.Single recipient, long callId) {
}
@Override
public List<TurnServer> getTurnServerInfo() {
return List.of();
}
@Override
public void close() {
}
@Override
public void addCallEventListener(CallEventListener listener) {
}
@Override
public void removeCallEventListener(CallEventListener listener) {
}
}
}

View File

@ -333,8 +333,7 @@ class SubscribeCallEventsTest {
}
@Override
public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> r) {
return null;
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> r) {
}
@Override