mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-08-21 04:46:06 +00:00
Merge branch 'master' into get-rate-limit-status
This commit is contained in:
commit
862c73cad1
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@ -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:
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@ -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
|
||||
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
323
client/Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" }
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,7 +1,9 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
31
gradlew.bat
vendored
@ -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%
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,6 +231,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||
userAgent,
|
||||
account.getCredentialsProvider(),
|
||||
account.getSignalServiceDataStore(),
|
||||
0,
|
||||
null,
|
||||
new ReentrantSignalSessionLock());
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = (
|
||||
"""
|
||||
|
||||
@ -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) {}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
0.90.0
|
||||
0.94.1
|
||||
|
||||
@ -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>]
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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" ]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
57
reproducible-builds/update-pinned-container-versions.sh
Executable file
57
reproducible-builds/update-pinned-container-versions.sh
Executable 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
|
||||
37
run_tests.sh
37
run_tests.sh
@ -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
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
81
src/main/java/org/asamk/signal/ConfigLoader.java
Normal file
81
src/main/java/org/asamk/signal/ConfigLoader.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
36
src/main/java/org/asamk/signal/GlobalConfig.java
Normal file
36
src/main/java/org/asamk/signal/GlobalConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
536
src/test/java/org/asamk/signal/http/SseInitialFlushTest.java
Normal file
536
src/test/java/org/asamk/signal/http/SseInitialFlushTest.java
Normal 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -333,8 +333,7 @@ class SubscribeCallEventsTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> r) {
|
||||
return null;
|
||||
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> r) {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user