Merge branch 'master' of https://github.com/AsamK/signal-cli into fix/unauthenticated-socket-keepalive

This commit is contained in:
Stefan Meinecke 2026-04-17 11:49:13 +02:00
commit f27eb524de
78 changed files with 912 additions and 508 deletions

57
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: build
on:
push:
branches:
- "**"
pull_request:
workflow_call:
permissions: {}
jobs:
build:
strategy:
matrix:
# The "reproducible" entry is used to build the project with the LTS Java version used in reproducible builds script.
# More Java versions can be added to test compatibility, eg. "26".
java: ["reproducible", "26"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build
run: |
if [ "${{ matrix.java }}" != "reproducible" ]; 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 }}
path: dist/*
build-client:
strategy:
matrix:
os:
- ubuntu
- macos
- windows
runs-on: ${{ matrix.os }}-latest
defaults:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v6
- name: Install rust
run: rustup default stable
- name: Build client
run: cargo build --release --verbose
- name: Archive production artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-client-${{ matrix.os }}
path: |
client/target/release/signal-cli-client
client/target/release/signal-cli-client.exe

View File

@ -1,96 +0,0 @@
name: signal-cli CI
on:
push:
branches:
- '**'
pull_request:
workflow_call:
permissions:
contents: write # to fetch code (actions/checkout) and submit dependency graph (gradle/gradle-build-action)
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '25', '26' ]
steps:
- uses: actions/checkout@v6
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
dependency-graph: generate-and-submit
- name: Install asciidoc
run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base
- name: Build with Gradle
run: ./gradlew --no-daemon build
- name: Build man page
run: |
cd man
make install
- name: Add man page to archive
run: |
version=$(tar tf build/distributions/signal-cli-*.tar | head -n1 | sed 's|signal-cli-\([^/]*\)/.*|\1|')
echo $version
tar --transform="flags=r;s|man|signal-cli-${version}/man|" -rf build/distributions/signal-cli-${version}.tar man/man{1,5}
- name: Compress archive
run: gzip -n -9 build/distributions/signal-cli-*.tar
- name: Archive production artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-archive-${{ matrix.java }}
path: build/distributions/signal-cli-*.tar.gz
build-graalvm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: graalvm/setup-graalvm@v1
with:
distribution: 'graalvm'
java-version: '25'
cache: 'gradle'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build with Gradle
run: ./gradlew --no-daemon nativeCompile
- name: Archive production artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-native
path: build/native/nativeCompile/signal-cli
build-client:
strategy:
matrix:
os:
- ubuntu
- macos
- windows
runs-on: ${{ matrix.os }}-latest
defaults:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v6
- name: Install rust
run: rustup default stable
- name: Build client
run: cargo build --release --verbose
- name: Archive production artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-client-${{ matrix.os }}
path: |
client/target/release/signal-cli-client
client/target/release/signal-cli-client.exe

View File

@ -5,8 +5,7 @@ on:
tags:
- v*
permissions:
contents: write # to fetch code (actions/checkout) and create release
permissions: {}
env:
IMAGE_NAME: signal-cli
@ -15,96 +14,25 @@ env:
REGISTRY_PASSWORD: ${{ github.token }}
jobs:
build:
uses: AsamK/signal-cli/.github/workflows/build.yml@master
ci_wf:
permissions:
contents: write
uses: AsamK/signal-cli/.github/workflows/ci.yml@master
# ${{ github.repository }} not accepted here
lib_to_jar:
needs: ci_wf
release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
signal_cli_version: ${{ steps.cli_ver.outputs.version }}
release_id: ${{ steps.create_release.outputs.id }}
version: ${{ steps.version.outputs.version }}
steps:
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8
- name: Get signal-cli version
id: cli_ver
id: version
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Extract archive
run: |
tree .
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
rm -rf signal-cli-archive-*/
# - name: Get signal-client jar version
# id: lib_ver
# run: |
# JAR_PREFIX=libsignal-client-
# jar_file=$(find ./signal-cli-*/lib/ -name "$JAR_PREFIX*.jar")
# jar_version=$(echo "$jar_file" | xargs basename | sed "s/$JAR_PREFIX//; s/.jar//")
# echo "$jar_version"
# echo "signal_client_version=${jar_version}" >> $GITHUB_OUTPUT
#
# - name: Download signal-client builds
# env:
# RELEASES_URL: https://github.com/signalapp/libsignal/releases/download/
# FILE_NAMES: signal_jni.dll libsignal_jni.dylib
# SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
# run: |
# for file_name in $FILE_NAMES; do
# curl -sOL "${RELEASES_URL}/v${SIGNAL_CLIENT_VER}/${file_name}" # note: added v
# done
# tree .
- name: Compress native app
env:
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
run: |
chmod +x signal-cli-native/signal-cli
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-native.tar.gz -C signal-cli-native signal-cli
rm -rf signal-cli-native/
- name: Compress client app
env:
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
run: |
chmod +x signal-cli-client-ubuntu/signal-cli-client
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-client.tar.gz -C signal-cli-client-ubuntu signal-cli-client
rm -rf signal-cli-client-ubuntu/
# - name: Replace Windows lib
# env:
# SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
# SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
# run: |
# mv signal_jni.dll libsignal_jni.so
# zip -u ./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar ./libsignal_jni.so
# tar -czf signal-cli-${SIGNAL_CLI_VER}-Windows.tar.gz signal-cli-*/
#
# - name: Replace macOS lib
# env:
# SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
# SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
# run: |
# jar_file=./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar
# zip -d $jar_file libsignal_jni.so
# zip $jar_file libsignal_jni.dylib
# tar -czf signal-cli-${SIGNAL_CLI_VER}-macOS.tar.gz signal-cli-*/
mv ./signal-cli-reproducible-build/* .
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Create release
id: create_release
@ -112,8 +40,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
release_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
tag_name: v${{ steps.version.outputs.version }} # note: added `v`
release_name: v${{ steps.version.outputs.version }} # note: added `v`
draft: true
- name: Upload archive
@ -122,19 +50,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
# - name: Upload Linux archive
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
# asset_content_type: application/x-compressed-tar # .tar.gz
asset_path: signal-cli-${{ steps.version.outputs.version }}.tar.gz
asset_name: signal-cli-${{ steps.version.outputs.version }}.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
- name: Upload Linux native archive
uses: actions/upload-release-asset@v1
@ -142,9 +60,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
asset_path: signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
- name: Upload Linux client archive
uses: actions/upload-release-asset@v1
@ -152,35 +70,14 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-client.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-client.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
# - name: Upload windows archive
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
# asset_content_type: application/x-compressed-tar # .tar.gz
#
# - name: Upload macos archive
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
# asset_content_type: application/x-compressed-tar # .tar.gz
asset_path: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
build-container:
needs: ci_wf
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
@ -188,28 +85,19 @@ jobs:
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file
run: |
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
rm -r signal-cli-archive-* signal-cli-native
tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}.tar.gz
mkdir -p build/install/
mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
- name: Build Image
id: build_image
uses: redhat-actions/buildah-build@v2
with:
image: ${{ env.IMAGE_NAME }}
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
containerfiles:
./Containerfile
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
containerfiles: ./Containerfile
oci: true
- name: Push To GHCR
@ -227,10 +115,9 @@ jobs:
echo "${{ toJSON(steps.push.outputs) }}"
build-container-native:
needs: ci_wf
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
@ -238,26 +125,20 @@ jobs:
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file
run: |
tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
mkdir -p build/native/nativeCompile/
chmod +x ./signal-cli-native/signal-cli
mv ./signal-cli-native/signal-cli build/native/nativeCompile/
mv signal-cli build/native/nativeCompile/
chmod +x build/native/nativeCompile/signal-cli
- name: Build Image
id: build_image
uses: redhat-actions/buildah-build@v2
with:
image: ${{ env.IMAGE_NAME }}
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
containerfiles:
./native.Containerfile
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
containerfiles: ./native.Containerfile
oci: true
- name: Push To GHCR
@ -275,10 +156,9 @@ jobs:
echo "${{ toJSON(steps.push.outputs) }}"
build-container-client:
needs: ci_wf
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
@ -286,26 +166,20 @@ jobs:
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file
run: |
tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
mkdir -p client/target/release/
chmod +x ./signal-cli-client-ubuntu/signal-cli-client
mv ./signal-cli-client-ubuntu/signal-cli-client client/target/release/
mv signal-cli-client client/target/release/
chmod +x client/target/release/signal-cli-client
- name: Build Image
id: build_image
uses: redhat-actions/buildah-build@v2
with:
image: ${{ env.IMAGE_NAME }}
tags: latest-client ${{ github.sha }}-client ${{ steps.cli_ver.outputs.version }}-client
containerfiles:
./client.Containerfile
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
containerfiles: ./client.Containerfile
oci: true
- name: Push To GHCR

7
.gitignore vendored
View File

@ -1,4 +1,5 @@
.gradle/
.kotlin/
.idea/*
!.idea/codeStyles/
build/
@ -13,3 +14,9 @@ out/
.DS_Store
/bin/
/test-config/
/dist/
/github/
man/*.1
man/*.5
man/man1
man/man5

View File

@ -2,6 +2,10 @@
## [Unreleased]
### 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.
## [0.14.2] - 2026-04-04
### Added

View File

@ -148,6 +148,16 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
./gradlew run --args="--help"
```
### JSON Schemas for the JSON-RPC mode
1. Generate [JSON Schema](https://json-schema.org/) files for all the JSON-RPC data classes (`src/main/java/org/asamk/signal/json`):
```sh
./gradlew jsonSchemas
```
2. The generated files can be found in the `build/generated/META-INF/schemas` folder.
### Building a native binary with GraalVM (EXPERIMENTAL)
It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not

View File

@ -1,3 +1,5 @@
import groovy.json.JsonOutput
plugins {
java
application
@ -72,6 +74,11 @@ val excludePatterns = mapOf(
)
)
val schemaAnnotationProcessor by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
registerTransform(JarFileExcluder::class) {
from.attribute(minified, false).attribute(artifactType, "jar")
@ -82,6 +89,8 @@ dependencies {
}
}
schemaAnnotationProcessor(libs.micronaut.json.schema.processor)
schemaAnnotationProcessor(libs.micronaut.inject.java)
implementation(libs.bouncycastle)
implementation(libs.jackson.databind)
implementation(libs.argparse4j)
@ -90,6 +99,10 @@ dependencies {
implementation(libs.slf4j.jul)
implementation(libs.logback)
implementation(libs.zxing)
implementation(libs.micronaut.json.schema.annotations)
if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) {
implementation(libs.micronaut.json.schema.generator)
}
implementation(project(":libsignal-cli"))
testImplementation(libs.junit.jupiter)
@ -160,3 +173,30 @@ tasks.register("writeLibsignalVersion") {
}
}
}
tasks.register<JavaCompile>("jsonSchemas") {
dependsOn(tasks.compileJava)
val schemaBaseUri = "http://localhost:8080/schemas/"
source = sourceSets.main.get().java
include("org/asamk/signal/json/**/*.java")
classpath = sourceSets.main.get().compileClasspath + files(sourceSets.main.get().java.destinationDirectory)
destinationDirectory.set(layout.buildDirectory.dir("generated"))
options.annotationProcessorPath = schemaAnnotationProcessor
options.compilerArgs.addAll(
listOf(
"-Amicronaut.processing.group=org.asamk",
"-Amicronaut.processing.module=signal-cli",
"-Amicronaut.processing.annotations=org.asamk.signal.json.*",
"-Amicronaut.jsonschema.baseUri=$schemaBaseUri",
)
)
doLast {
fileTree(destinationDirectory.get().dir("META-INF/schemas").asFile) {
include("*.schema.json")
}.forEach { schemaFile ->
val normalized = schemaFile.readText().replace("\"$schemaBaseUri/", "\"")
val prettyJson = JsonOutput.prettyPrint(normalized)
schemaFile.writeText("$prettyJson\n")
}
}
}

323
client/Cargo.lock generated
View File

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

View File

@ -1,19 +1,25 @@
[versions]
slf4j = "2.0.17"
junit = "6.0.2"
junit = "6.0.3"
micronaut-json-schema = "2.0.0-M8"
micronaut-core = "4.9.3"
[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"
zxing = "com.google.zxing:core:3.5.4"
micronaut-json-schema-annotations = { module = "io.micronaut.jsonschema:micronaut-json-schema-annotations", version.ref = "micronaut-json-schema" }
micronaut-json-schema-processor = { module = "io.micronaut.jsonschema:micronaut-json-schema-processor", version.ref = "micronaut-json-schema" }
micronaut-json-schema-generator = { module = "io.micronaut.jsonschema:micronaut-json-schema-generator", version.ref = "micronaut-json-schema" }
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-core" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.5.32"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_143"
sqlite = "org.xerial:sqlite-jdbc:3.51.2.0"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_144"
sqlite = "org.xerial:sqlite-jdbc:3.53.0.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" }

View File

@ -2,11 +2,11 @@ package org.asamk.signal.manager.api;
public class CaptchaRequiredException extends Exception {
private long nextAttemptTimestamp;
private long nextVerificationAttemptMilliseconds;
public CaptchaRequiredException(final long nextAttemptTimestamp) {
public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) {
super("Captcha required");
this.nextAttemptTimestamp = nextAttemptTimestamp;
this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds;
}
public CaptchaRequiredException(final String message) {
@ -17,7 +17,7 @@ public class CaptchaRequiredException extends Exception {
super(message, cause);
}
public long getNextAttemptTimestamp() {
return nextAttemptTimestamp;
public long getNextVerificationAttemptMilliseconds() {
return nextVerificationAttemptMilliseconds;
}
}

View File

@ -10,12 +10,19 @@ public class ProofRequiredException extends Exception {
private final String token;
private final Set<Option> options;
private final long retryAfterSeconds;
private final long retryAfterMilliseconds;
public ProofRequiredException(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
this.token = e.getToken();
this.options = e.getOptions().stream().map(Option::from).collect(Collectors.toSet());
this.retryAfterSeconds = e.getRetryAfterSeconds();
public ProofRequiredException(final String token, final Set<Option> options, final long retryAfterMilliseconds) {
super("Rate limit");
this.token = token;
this.options = options;
this.retryAfterMilliseconds = retryAfterMilliseconds;
}
public static ProofRequiredException from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
return new ProofRequiredException(e.getToken(),
e.getOptions().stream().map(Option::from).collect(Collectors.toSet()),
e.getRetryAfterSeconds() * 1000L);
}
public String getToken() {
@ -26,8 +33,8 @@ public class ProofRequiredException extends Exception {
return options;
}
public long getRetryAfterSeconds() {
return retryAfterSeconds;
public long getRetryAfterMilliseconds() {
return retryAfterMilliseconds;
}
public enum Option {

View File

@ -2,14 +2,18 @@ package org.asamk.signal.manager.api;
public class RateLimitException extends Exception {
private final long nextAttemptTimestamp;
private final Long retryAfterMilliseconds;
public RateLimitException(final long nextAttemptTimestamp) {
public RateLimitException(final Long retryAfterMilliseconds) {
super("Rate limit");
this.nextAttemptTimestamp = nextAttemptTimestamp;
this.retryAfterMilliseconds = retryAfterMilliseconds;
}
public long getNextAttemptTimestamp() {
return nextAttemptTimestamp;
public static RateLimitException from(org.whispersystems.signalservice.api.push.exceptions.RateLimitException e) {
return new RateLimitException(e.getRetryAfterMilliseconds().orElse(null));
}
public Long getRetryAfterMilliseconds() {
return retryAfterMilliseconds;
}
}

View File

@ -9,13 +9,13 @@ public record SendMessageResult(
boolean isNetworkFailure,
boolean isUnregisteredFailure,
boolean isIdentityFailure,
boolean isRateLimitFailure,
RateLimitException rateLimitException,
ProofRequiredException proofRequiredFailure,
boolean isInvalidPreKeyFailure
) {
public static SendMessageResult unregisteredFailure(RecipientAddress address) {
return new SendMessageResult(address, false, false, true, false, false, null, false);
return new SendMessageResult(address, false, false, true, false, null, null, false);
}
public static SendMessageResult from(
@ -23,16 +23,30 @@ public record SendMessageResult(
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
final var rateLimitFailure = sendMessageResult.getRateLimitFailure();
final var proofRequiredFailure = sendMessageResult.getProofRequiredFailure();
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
sendMessageResult.getAddress())).toApiRecipientAddress(),
sendMessageResult.isSuccess(),
sendMessageResult.isNetworkFailure(),
sendMessageResult.isUnregisteredFailure(),
sendMessageResult.getIdentityFailure() != null,
sendMessageResult.getRateLimitFailure() != null || sendMessageResult.getProofRequiredFailure() != null,
sendMessageResult.getProofRequiredFailure() == null
? null
: new ProofRequiredException(sendMessageResult.getProofRequiredFailure()),
rateLimitFailure == null ? null : RateLimitException.from(rateLimitFailure),
proofRequiredFailure == null ? null : ProofRequiredException.from(proofRequiredFailure),
sendMessageResult.isInvalidPreKeyFailure());
}
public boolean isRateLimitFailure() {
return this.rateLimitException != null || this.proofRequiredFailure != null;
}
public Long rateLimitRetryAfterMilliseconds() {
if (proofRequiredFailure != null) {
return proofRequiredFailure.getRetryAfterMilliseconds();
} else if (rateLimitException != null) {
return rateLimitException.getRetryAfterMilliseconds();
} else {
return null;
}
}
}

View File

@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
@ -26,4 +27,18 @@ public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<S
.flatMap(res -> res.stream().map(SendMessageResult::isRateLimitFailure))
.allMatch(r -> r) && results.values().stream().mapToInt(List::size).sum() > 0;
}
/**
* Longest rate-limit retry-after window across all rate-limited recipients, in milliseconds.
* Null when no recipient reported one (server omitted Retry-After, or no rate-limit failures).
*/
public Long maxRateLimitRetryAfterMilliseconds() {
return results.values()
.stream()
.flatMap(List::stream)
.map(SendMessageResult::rateLimitRetryAfterMilliseconds)
.filter(Objects::nonNull)
.max(Long::compareTo)
.orElse(null);
}
}

View File

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

View File

@ -134,7 +134,9 @@ public final class ProfileHelper {
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL,
false));
} catch (IOException e) {
logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
logger.warn("Failed to retrieve profile key credential for {}, ignoring: {}",
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
e.getMessage());
return null;
}
@ -263,7 +265,9 @@ public final class ProfileHelper {
try {
blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE, false));
} catch (IOException e) {
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
logger.warn("Failed to retrieve profile for {}, ignoring: {}",
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
e.getMessage());
}
return account.getProfileStore().getProfile(recipientId);
@ -381,7 +385,9 @@ public final class ProfileHelper {
logger.trace("Done handling retrieved profile");
}).doOnError(e -> {
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
logger.warn("Failed to retrieve profile for {}, ignoring: {}",
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
e.getMessage());
final var profile = account.getProfileStore().getProfile(recipientId);
final var newProfile = (
profile == null ? Profile.newBuilder() : Profile.newBuilder(profile)

View File

@ -278,7 +278,7 @@ public class ManagerImpl implements Manager {
registeredUsers = context.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet);
} catch (CdsiResourceExhaustedException e) {
logger.debug("CDSI resource exhausted: {}", e.getMessage());
throw new RateLimitException(System.currentTimeMillis() + e.getRetryAfterSeconds() * 1000L);
throw new RateLimitException(e.getRetryAfterSeconds() * 1000L);
}
return numbers.stream().collect(Collectors.toMap(n -> n, n -> {

View File

@ -65,14 +65,12 @@ public class NumberVerificationUtils {
if (nextAttempt == null) {
throw new VerificationMethodNotAvailableException();
} else if (nextAttempt > 0) {
final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextAttempt * 1000;
throw new RateLimitException(timestamp);
throw new RateLimitException(nextAttempt * 1000L);
}
final var nextVerificationAttempt = sessionResponse.getMetadata().getNextVerificationAttempt();
if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextVerificationAttempt * 1000;
throw new CaptchaRequiredException(timestamp);
throw new CaptchaRequiredException(nextVerificationAttempt * 1000L);
}
if (sessionResponse.getMetadata().getRequestedInformation().contains("captcha")) {

View File

@ -14,8 +14,8 @@ all: $(MANPAGESRC)
.PHONY: install
install: all
$(MKDIR) -p man1 man5
for f in *.1; do $(GZIP) < "$$f" > man1/"$$f".gz ; done
for f in *.5; do $(GZIP) < "$$f" > man5/"$$f".gz ; done
for f in *.1; do $(GZIP) -n < "$$f" > man1/"$$f".gz ; done
for f in *.5; do $(GZIP) -n < "$$f" > man5/"$$f".gz ; done
.PHONY: clean
clean:

View File

@ -1168,6 +1168,7 @@ signal-cli -a ACCOUNT trust -a RECIPIENT
* *3*: Server or IO error
* *4*: Sending failed due to untrusted key
* *5*: Server rate limiting error
* *6*: CAPTCHA was rejected
== Files

View File

@ -0,0 +1,34 @@
# Reproducible builds
This process lets you verify that the version of signal-cli that was downloaded from the Github Releases matches the source code in the public repository.
This is achieved by replicating the build environment as Docker images.
Currently, only the following binaries are reproducible:
- [x] JAR package (`signal-cli-XXX.tar.gz`)
- [ ] Native binary (`signal-cli-XXX-Linux-native.tar.gz`)
- [x] Rust client binary (`signal-cli-XXX-Linux-client.tar.gz`)
In the following section, we will use signal-cli version 0.14.2 as the reference example. Simply replace all occurrences of 0.14.2 with the version number you are about to verify.
## Step-by-step instructions
### 0. Prerequisites
Before you begin, ensure you have the following installed:
- git
- docker (or podman)
### 1. Verifying reproducibility
```bash
git clone --depth 1 --branch v0.14.2 https://github.com/AsamK/signal-cli
cd ./signal-cli
./reproducible-builds/verify.sh
```
If each one ends with `... matches!` for every binary (except the native one for now), you're good to go! You've successfully verified that the Github Release binaries were built from exactly the same code as is in the signal-cli git repository.
If you get `... doesn't match!`, it means something went wrong (except for the native one for now). Please [open an issue](https://github.com/AsamK/signal-cli/issues/new/choose).

View File

@ -0,0 +1,13 @@
ARG ZULU_TAG="25.0.2-jdk@sha256:9582df6c4415d9c770eb5ff8fce426ebba53631149c9eb083ee126568d32fab3"
FROM docker.io/azul/zulu-openjdk:$ZULU_TAG
ENV SOURCE_DATE_EPOCH=1767225600
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
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
WORKDIR /signal-cli
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ]

50
reproducible-builds/build.sh Executable file
View File

@ -0,0 +1,50 @@
#!/bin/bash
set -eu
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../"
cd "$ROOT_DIR"
rm -rf "$ROOT_DIR/dist"
mkdir -p "$ROOT_DIR/dist"
if command -v podman >/dev/null; then
ENGINE=podman
USER=
else
ENGINE=docker
USER="--user $(id -u):$(id -g)"
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
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
mv build/distributions/signal-cli-*.tar.gz dist/
if [ -n "${OVERRIDE_JAVA_VERSION:-}" ]; then
echo -e "\e[33mBuild was performed with overridden Java version $OVERRIDE_JAVA_VERSION, native-image and client will not be built.\e[0m"
exit 0
fi
# Build native-image
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
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
mv build/signal-cli-*-Linux-client.tar.gz dist/
ls -lsh dist/
echo -e "\e[32mBuild successful!\e[0m"

View File

@ -0,0 +1,7 @@
FROM docker.io/rust:1.94.1-slim-trixie@sha256:c6a474d7164ea2455e09b60a759b1edca38db7373c5689c1dae31780de4e71ac
ENV SOURCE_DATE_EPOCH=1767225600
ENV LANG=C.UTF-8
ENV LC_CTYPE=en_US.UTF-8
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
WORKDIR /signal-cli
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "client" ]

View File

@ -0,0 +1,78 @@
#!/bin/bash
set -eu
echo "Build '$1' variant $VERSION ..."
function reset_file_dates() {
find . -exec touch -m -d "@$SOURCE_DATE_EPOCH" {} \;
}
reset_file_dates
if [ "$1" == "build" ]; then
./gradlew build \
--no-daemon \
--max-workers=1 \
-Dkotlin.compiler.execution.strategy=in-process \
--no-build-cache \
-Dorg.gradle.caching=false \
-Porg.gradle.java.installations.auto-download=false \
-Porg.gradle.java.installations.auto-detect=false
cd man
make install
cd ..
tar_archive="build/distributions/signal-cli-${VERSION}.tar"
tar --transform="flags=r;s|man|signal-cli-${VERSION}/man|" -rf "$tar_archive" man/man{1,5}
# Remake the tarball to ensure reproducible file order and timestamps
mkdir -p build/extracted
tar -xf "$tar_archive" -C build/extracted/
reset_file_dates
rm -f "$tar_archive"
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --transform='s|^\./||' --owner=0 --group=0 --numeric-owner -cf "$tar_archive" -C build/extracted .
gzip -n -9 "$tar_archive"
elif [ "$1" == "native" ]; then
./gradlew nativeCompile \
--no-daemon \
--max-workers=1 \
-Dkotlin.compiler.execution.strategy=in-process \
--no-build-cache \
-Dorg.gradle.caching=false \
-Dgraalvm.native-image.build-time=2026-01-01T00:00:00Z \
-Porg.gradle.java.installations.auto-download=false \
-Porg.gradle.java.installations.auto-detect=false
strip --strip-all \
--remove-section=.note.gnu.build-id \
--remove-section=.comment \
--remove-section=.gnu_debuglink \
--remove-section=.annobin.notes \
--remove-section=.gnu.build.attributes \
--remove-section=.note.ABI-tag \
build/native/nativeCompile/signal-cli
chmod +x build/native/nativeCompile/signal-cli
reset_file_dates
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner -cf "build/signal-cli-${VERSION}-Linux-native.tar" -C build/native/nativeCompile signal-cli
gzip -n -9 "build/signal-cli-${VERSION}-Linux-native.tar"
elif [ "$1" == "client" ]; then
cd client
cargo build --release --locked
cd ..
chmod +x client/target/release/signal-cli-client
mkdir -p build
reset_file_dates
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner -cf "build/signal-cli-${VERSION}-Linux-client.tar" -C client/target/release signal-cli-client
gzip -n -9 "build/signal-cli-${VERSION}-Linux-client.tar"
else
echo "Unknown build variant '$1'"
exit 1
fi

View File

@ -0,0 +1,7 @@
FROM container-registry.oracle.com/graalvm/native-image:25.0.2@sha256:4c0d5919f6840d89721274eb8cf81962faa2f870b816967e6732e2a151b150d8
ENV SOURCE_DATE_EPOCH=1767225600
ENV LANG=C.UTF-8
ENV LC_CTYPE=en_US.UTF-8
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
WORKDIR /signal-cli
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "native" ]

44
reproducible-builds/verify.sh Executable file
View File

@ -0,0 +1,44 @@
#!/bin/bash
set -eu
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../"
cd "$ROOT_DIR"
rm -rf "$ROOT_DIR/github"
mkdir -p "$ROOT_DIR/github"
VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1)
echo "Download latest release from GitHub..."
curl -L --fail "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}.tar.gz" -o "github/signal-cli-${VERSION}.tar.gz"
curl -L --fail "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}-Linux-native.tar.gz" -o "github/signal-cli-${VERSION}-Linux-native.tar.gz"
curl -L --fail "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}-Linux-client.tar.gz" -o "github/signal-cli-${VERSION}-Linux-client.tar.gz"
./reproducible-builds/build.sh
rm -f {github,dist}/VERSION
echo "commit: $(git rev-parse HEAD)"
echo "sha256 hashes of GitHub release:"
sha256sum github/*
echo "sha256 hashes of locally built files:"
sha256sum dist/*
reproducible=true
for file in $(cd github && find . -type f); do
if diff "github/$file" "dist/$file" >/dev/null 2>&1; then
echo -e "\e[32m[+] '$(basename "$file")' matches!\e[0m"
elif [[ "$file" =~ "native" ]]; then
echo -e "\e[33m[-] '$(basename "$file")' doesn't match! (not supported yet)\e[0m"
reproducible=false
else
echo -e "\e[31m[-] '$(basename "$file")' doesn't match!\e[0m"
reproducible=false
fi
done
if [ "$reproducible" = false ]; then
exit 1
fi

View File

@ -8,7 +8,7 @@ public class BaseConfig {
public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion();
static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
.orElse("Signal-Android/8.6.1");
.orElse("Signal-Android/8.8.0");
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
? "signal-cli"
: PROJECT_NAME + "/" + PROJECT_VERSION;

View File

@ -22,6 +22,7 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.commands.exceptions.CaptchaRejectedErrorException;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.RateLimitErrorException;
@ -128,6 +129,7 @@ public class Main {
case IOErrorException ioErrorException -> 3;
case UntrustedKeyErrorException untrustedKeyErrorException -> 4;
case RateLimitErrorException rateLimitErrorException -> 5;
case CaptchaRejectedErrorException captchaRejectedErrorException -> 6;
case null -> 2;
};
}

View File

@ -3,9 +3,9 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CaptchaRejectedErrorException;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.CaptchaRejectedException;
import org.asamk.signal.output.OutputWriter;
@ -41,8 +41,8 @@ public class SubmitRateLimitChallengeCommand implements JsonRpcLocalCommand {
} catch (IOException e) {
throw new IOErrorException("Submit challenge error: " + e.getMessage(), e);
} catch (CaptchaRejectedException e) {
throw new UserErrorException(
"Captcha rejected, it may be outdated, already used or solved from a different IP address.");
throw new CaptchaRejectedErrorException(
"Captcha rejected, it may be outdated, already used or solved from a different IP address.", e);
}
}
}

View File

@ -0,0 +1,10 @@
package org.asamk.signal.commands.exceptions;
import org.asamk.signal.manager.api.CaptchaRejectedException;
public final class CaptchaRejectedErrorException extends CommandException {
public CaptchaRejectedErrorException(final String message, final CaptchaRejectedException cause) {
super(message, cause);
}
}

View File

@ -1,6 +1,6 @@
package org.asamk.signal.commands.exceptions;
public sealed abstract class CommandException extends Exception permits IOErrorException, RateLimitErrorException, UnexpectedErrorException, UntrustedKeyErrorException, UserErrorException {
public sealed abstract class CommandException extends Exception permits CaptchaRejectedErrorException, IOErrorException, RateLimitErrorException, UnexpectedErrorException, UntrustedKeyErrorException, UserErrorException {
public CommandException(final String message) {
super(message);

View File

@ -702,9 +702,13 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (RateLimitException e) {
throw new Error.Failure(e.getMessage()
+ ", retry at "
+ DateUtils.formatTimestamp(e.getNextAttemptTimestamp()));
final var retryAfterMilliseconds = e.getRetryAfterMilliseconds();
throw new Error.Failure(e.getMessage() + (
retryAfterMilliseconds == null
? ""
: ", retry at " + DateUtils.formatTimestamp(System.currentTimeMillis()
+ retryAfterMilliseconds)
));
}
return numbers.stream().map(number -> registered.get(number).uuid() != null).toList();

View File

@ -1,9 +1,11 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.UUID;
@JsonSchema(title = "AdminDelete")
public record JsonAdminDelete(
@Deprecated String targetAuthor, String targetAuthorNumber, String targetAuthorUuid, long targetSentTimestamp
) {

View File

@ -1,7 +1,10 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "Attachment")
record JsonAttachment(
String contentType,
String filename,

View File

@ -1,5 +1,8 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
@JsonSchema(title = "AttachmentData")
public record JsonAttachmentData(
String data
) {}

View File

@ -1,6 +1,7 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
@ -10,6 +11,7 @@ import java.util.List;
import static org.asamk.signal.manager.util.Utils.callIdUnsigned;
@JsonSchema(title = "CallMessage")
record JsonCallMessage(
@JsonInclude(JsonInclude.Include.NON_NULL) Offer offerMessage,
@JsonInclude(JsonInclude.Include.NON_NULL) Answer answerMessage,

View File

@ -1,9 +1,11 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import java.util.List;
@JsonSchema(title = "Contact")
public record JsonContact(
String number,
String uuid,
@ -26,6 +28,7 @@ public record JsonContact(
@JsonInclude(JsonInclude.Include.NON_NULL) JsonInternal internal
) {
@JsonSchema(title = "Profile")
public record JsonProfile(
long lastUpdateTimestamp,
String givenName,
@ -36,6 +39,7 @@ public record JsonContact(
String mobileCoinAddress
) {}
@JsonSchema(title = "Internal")
public record JsonInternal(
List<String> capabilities,
String unidentifiedAccessMode,

View File

@ -1,8 +1,11 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.util.Util;
@JsonSchema(title = "ContactAddress")
public record JsonContactAddress(
String type,
String label,

View File

@ -1,7 +1,10 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "ContactAvatar")
public record JsonContactAvatar(JsonAttachment attachment, boolean isProfile) {
static JsonContactAvatar from(MessageEnvelope.Data.SharedContact.Avatar avatar) {

View File

@ -1,8 +1,11 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.util.Util;
@JsonSchema(title = "ContactEmail")
public record JsonContactEmail(String value, String type, String label) {
static JsonContactEmail from(MessageEnvelope.Data.SharedContact.Email email) {

View File

@ -1,8 +1,11 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.util.Util;
@JsonSchema(title = "ContactName")
public record JsonContactName(
String nickname, String given, String family, String prefix, String suffix, String middle
) {

View File

@ -1,8 +1,11 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.util.Util;
@JsonSchema(title = "ContactPhone")
public record JsonContactPhone(String value, String type, String label) {
static JsonContactPhone from(MessageEnvelope.Data.SharedContact.Phone phone) {

View File

@ -1,12 +1,14 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.List;
@JsonSchema(title = "DataMessage")
record JsonDataMessage(
long timestamp,
String message,

View File

@ -1,8 +1,11 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "EditMessage")
record JsonEditMessage(long targetSentTimestamp, JsonDataMessage dataMessage) {
static JsonEditMessage from(MessageEnvelope.Edit editMessage, Manager m) {

View File

@ -1,5 +1,8 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
@JsonSchema(title = "Error")
public record JsonError(String message, String type) {
public static JsonError from(Throwable exception) {

View File

@ -1,8 +1,11 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "GroupInfo")
record JsonGroupInfo(String groupId, String groupName, int revision, String type) {
static JsonGroupInfo from(MessageEnvelope.Data.GroupContext groupContext, Manager m) {

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.UUID;
@JsonSchema(title = "Mention")
public record JsonMention(@Deprecated String name, String number, String uuid, int start, int length) {
static JsonMention from(MessageEnvelope.Data.Mention mention) {

View File

@ -1,6 +1,7 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.MessageEnvelope;
@ -10,6 +11,7 @@ import org.asamk.signal.manager.api.UntrustedIdentityException;
import java.util.UUID;
@JsonSchema(title = "MessageEnvelope")
public record JsonMessageEnvelope(
@Deprecated String source,
String sourceNumber,

View File

@ -1,7 +1,10 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "Payment")
public record JsonPayment(String note, byte[] receipt) {
static JsonPayment from(MessageEnvelope.Data.Payment payment) {

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.UUID;
@JsonSchema(title = "PinMessage")
public record JsonPinMessage(
@Deprecated String targetAuthor,
String targetAuthorNumber,

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.List;
@JsonSchema(title = "PollCreate")
public record JsonPollCreate(
String question, boolean allowMultiple, List<String> options
) {

View File

@ -1,7 +1,10 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "PollTerminate")
public record JsonPollTerminate(long targetSentTimestamp) {
static JsonPollTerminate from(MessageEnvelope.Data.PollTerminate pollTerminate) {

View File

@ -1,10 +1,13 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.List;
import java.util.UUID;
@JsonSchema(title = "PollVote")
public record JsonPollVote(
@Deprecated String author,
String authorNumber,

View File

@ -1,7 +1,10 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "Preview")
public record JsonPreview(String url, String title, String description, JsonAttachment image) {
static JsonPreview from(MessageEnvelope.Data.Preview preview) {

View File

@ -1,12 +1,14 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.List;
import java.util.UUID;
@JsonSchema(title = "Quote")
public record JsonQuote(
long id,
@Deprecated String author,

View File

@ -1,9 +1,11 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "QuotedAttachment")
public record JsonQuotedAttachment(
String contentType, String filename, @JsonInclude(JsonInclude.Include.NON_NULL) JsonAttachment thumbnail
) {

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.UUID;
@JsonSchema(title = "Reaction")
public record JsonReaction(
String emoji,
@Deprecated String targetAuthor,

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.List;
@JsonSchema(title = "ReceiptMessage")
record JsonReceiptMessage(long when, boolean isDelivery, boolean isRead, boolean isViewed, List<Long> timestamps) {
static JsonReceiptMessage from(MessageEnvelope.Receipt receiptMessage) {

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.RecipientAddress;
import java.util.UUID;
@JsonSchema(title = "RecipientAddress")
public record JsonRecipientAddress(String uuid, String number, String username) {
public static JsonRecipientAddress from(RecipientAddress address) {

View File

@ -1,3 +1,6 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
@JsonSchema(title = "RemoteDelete")
record JsonRemoteDelete(long timestamp) {}

View File

@ -1,10 +1,12 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.GroupId;
import org.asamk.signal.manager.api.SendMessageResult;
@JsonSchema(title = "SendMessageResult")
public record JsonSendMessageResult(
JsonRecipientAddress recipientAddress,
@JsonInclude(JsonInclude.Include.NON_NULL) String groupId,
@ -18,6 +20,7 @@ public record JsonSendMessageResult(
}
public static JsonSendMessageResult from(SendMessageResult result, GroupId groupId) {
final var rateLimitRetryAfterMilliseconds = result.rateLimitRetryAfterMilliseconds();
return new JsonSendMessageResult(JsonRecipientAddress.from(result.address()),
groupId != null ? groupId.toBase64() : null,
result.isSuccess()
@ -32,7 +35,7 @@ public record JsonSendMessageResult(
? Type.INVALID_PRE_KEY_FAILURE
: Type.IDENTITY_FAILURE,
result.proofRequiredFailure() != null ? result.proofRequiredFailure().getToken() : null,
result.proofRequiredFailure() != null ? result.proofRequiredFailure().getRetryAfterSeconds() : null);
rateLimitRetryAfterMilliseconds == null ? null : Math.ceilDiv(rateLimitRetryAfterMilliseconds, 1000L));
}
public enum Type {

View File

@ -1,11 +1,13 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.List;
@JsonSchema(title = "SharedContact")
public record JsonSharedContact(
JsonContactName name,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonContactAvatar avatar,

View File

@ -1,8 +1,11 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.util.Hex;
@JsonSchema(title = "Sticker")
public record JsonSticker(String packId, int stickerId) {
static JsonSticker from(MessageEnvelope.Data.Sticker sticker) {

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.UUID;
@JsonSchema(title = "StoryContext")
record JsonStoryContext(
String authorNumber, String authorUuid, long sentTimestamp
) {

View File

@ -1,6 +1,7 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.Color;
import org.asamk.signal.manager.api.GroupId;
@ -8,6 +9,7 @@ import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.List;
@JsonSchema(title = "StoryMessage")
record JsonStoryMessage(
boolean allowsReplies,
@JsonInclude(JsonInclude.Include.NON_NULL) String groupId,

View File

@ -2,6 +2,7 @@ package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.MessageEnvelope;
@ -9,6 +10,7 @@ import org.asamk.signal.manager.api.RecipientAddress;
import java.util.UUID;
@JsonSchema(title = "SyncDataMessage")
record JsonSyncDataMessage(
@Deprecated String destination,
String destinationNumber,

View File

@ -9,12 +9,15 @@ import org.asamk.signal.manager.api.RecipientAddress;
import java.util.List;
import io.micronaut.jsonschema.JsonSchema;
enum JsonSyncMessageType {
CONTACTS_SYNC,
GROUPS_SYNC,
REQUEST_SYNC
}
@JsonSchema(title = "SyncMessage")
record JsonSyncMessage(
@JsonInclude(JsonInclude.Include.NON_NULL) JsonSyncDataMessage sentMessage,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonSyncStoryMessage sentStoryMessage,

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.UUID;
@JsonSchema(title = "SyncReadMessage")
record JsonSyncReadMessage(
@Deprecated String sender, String senderNumber, String senderUuid, long timestamp
) {

View File

@ -1,11 +1,13 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.UUID;
@JsonSchema(title = "SyncStoryMessage")
record JsonSyncStoryMessage(
String destinationNumber, String destinationUuid, @JsonUnwrapped JsonStoryMessage dataMessage
) {

View File

@ -1,7 +1,10 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.TextStyle;
@JsonSchema(title = "TextStyle")
public record JsonTextStyle(String style, int start, int length) {
static JsonTextStyle from(TextStyle textStyle) {

View File

@ -1,10 +1,12 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.GroupId;
import org.asamk.signal.manager.api.MessageEnvelope;
@JsonSchema(title = "TypingMessage")
record JsonTypingMessage(
String action, long timestamp, @JsonInclude(JsonInclude.Include.NON_NULL) String groupId
) {

View File

@ -1,9 +1,12 @@
package org.asamk.signal.json;
import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.MessageEnvelope;
import java.util.UUID;
@JsonSchema(title = "UnpinMessage")
public record JsonUnpinMessage(
@Deprecated String targetAuthor, String targetAuthorNumber, String targetAuthorUuid, long targetSentTimestamp
) {

View File

@ -12,6 +12,7 @@ import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.JsonRpcMultiCommand;
import org.asamk.signal.commands.JsonRpcRegistrationCommand;
import org.asamk.signal.commands.JsonRpcSingleCommand;
import org.asamk.signal.commands.exceptions.CaptchaRejectedErrorException;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.RateLimitErrorException;
@ -39,6 +40,7 @@ public class SignalJsonRpcCommandHandler {
private static final int IO_ERROR = -3;
private static final int UNTRUSTED_KEY_ERROR = -4;
private static final int RATELIMIT_ERROR = -5;
private static final int CAPTCHA_REJECTED_ERROR = -6;
private final Manager m;
private final MultiAccountManager c;
@ -258,6 +260,10 @@ public class SignalJsonRpcCommandHandler {
case RateLimitErrorException e -> throw new JsonRpcException(new JsonRpcResponse.Error(RATELIMIT_ERROR,
e.getMessage(),
getErrorDataNode(objectMapper, result)));
case CaptchaRejectedErrorException e -> throw new JsonRpcException(new JsonRpcResponse.Error(
CAPTCHA_REJECTED_ERROR,
e.getMessage(),
getErrorDataNode(objectMapper, result)));
case UnexpectedErrorException e -> {
logger.error("Command execution failed with unexpected error", e);
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,

View File

@ -129,16 +129,19 @@ public class CommandUtil {
} else {
message = "Invalid captcha given.";
}
if (e.getNextAttemptTimestamp() > 0) {
message += "\nNext Captcha may be provided at " + DateUtils.formatTimestamp(e.getNextAttemptTimestamp());
if (e.getNextVerificationAttemptMilliseconds() > 0) {
message += "\nNext Captcha may be provided at " + DateUtils.formatTimestamp(System.currentTimeMillis()
+ e.getNextVerificationAttemptMilliseconds());
}
return message;
}
public static String getRateLimitMessage(final RateLimitException e) {
String message = "Rate limit reached";
if (e.getNextAttemptTimestamp() > 0) {
message += "\nNext attempt may be tried at " + DateUtils.formatTimestamp(e.getNextAttemptTimestamp());
final var retryAfterMilliseconds = e.getRetryAfterMilliseconds();
if (retryAfterMilliseconds != null) {
message += "\nNext attempt may be tried at " + DateUtils.formatTimestamp(System.currentTimeMillis()
+ retryAfterMilliseconds);
}
return message;
}

View File

@ -59,8 +59,10 @@ public class SendMessageResultUtils {
if (sendMessageResults.hasOnlyUntrustedIdentity()) {
throw new UntrustedKeyErrorException("Failed to send message due to untrusted identities");
} else if (sendMessageResults.hasOnlyRateLimitFailure()) {
final var retryAfter = sendMessageResults.maxRateLimitRetryAfterMilliseconds();
final var nextAttempt = retryAfter == null ? 0L : System.currentTimeMillis() + retryAfter;
throw new RateLimitErrorException("Failed to send message due to rate limiting",
new RateLimitException(0));
new RateLimitException(nextAttempt));
} else {
throw new UserErrorException("Failed to send message");
}
@ -106,11 +108,14 @@ public class SendMessageResultUtils {
.map(ProofRequiredException.Option::toString)
.collect(Collectors.joining(", ")),
failure.getToken(),
failure.getRetryAfterSeconds());
Math.ceilDiv(failure.getRetryAfterMilliseconds(), 1000L));
} else if (result.isNetworkFailure()) {
return String.format("Network failure for \"%s\"", identifier);
} else if (result.isRateLimitFailure()) {
return String.format("Rate limit failure for \"%s\"", identifier);
final var retryAfter = result.rateLimitRetryAfterMilliseconds();
return retryAfter != null ? String.format("Rate limit failure for \"%s\", retry after %d seconds",
identifier,
Math.ceilDiv(retryAfter, 1000L)) : String.format("Rate limit failure for \"%s\"", identifier);
} else if (result.isUnregisteredFailure()) {
return String.format("Unregistered user \"%s\"", identifier);
} else if (result.isIdentityFailure()) {

View File

@ -1564,6 +1564,9 @@
}
]
},
{
"type": "kotlin.reflect.jvm.internal.impl.km.jvm.internal.JvmMetadataExtensions"
},
{
"type": "kotlin.reflect.jvm.internal.impl.load.java.ErasedOverridabilityCondition"
},
@ -1626,6 +1629,9 @@
}
]
},
{
"type": "kotlinx.coroutines.CancelledContinuation"
},
{
"type": "kotlinx.coroutines.CompletedExceptionally",
"fields": [
@ -1681,6 +1687,9 @@
}
]
},
{
"type": "kotlinx.coroutines.internal.LimitedDispatcher"
},
{
"type": "kotlinx.coroutines.internal.LockFreeLinkedListNode",
"fields": [
@ -1714,6 +1723,9 @@
}
]
},
{
"type": "kotlinx.coroutines.internal.ThreadSafeHeap"
},
{
"type": "kotlinx.coroutines.scheduling.CoroutineScheduler",
"fields": [
@ -1728,6 +1740,12 @@
}
]
},
{
"type": "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker"
},
{
"type": "kotlinx.coroutines.scheduling.WorkQueue"
},
{
"type": "libcore.io.Memory"
},
@ -4898,6 +4916,15 @@
}
]
},
{
"type": "org.bouncycastle.jcajce.provider.kdf.PBKDF2$Mappings",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
},
{
"type": "org.bouncycastle.jcajce.provider.kdf.SCRYPT$Mappings",
"methods": [
@ -10115,6 +10142,9 @@
{
"glob": "META-INF/services/java.util.spi.ResourceBundleControlProvider"
},
{
"glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.km.internal.extensions.MetadataExtensions"
},
{
"glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition"
},

View File

@ -0,0 +1,114 @@
package org.asamk.signal.json;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.RecipientAddress;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendMessageResult;
import org.asamk.signal.manager.api.SendMessageResults;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class JsonSendMessageResultTest {
private static final RecipientAddress ADDRESS = new RecipientAddress(null, null, "+15551234567", null);
@Test
void rateLimitFailureSurfacesRetryAfterSeconds() {
var result = new SendMessageResult(ADDRESS,
false,
false,
false,
false,
new RateLimitException(3600_000L),
null,
false);
var json = JsonSendMessageResult.from(result);
assertEquals(JsonSendMessageResult.Type.RATE_LIMIT_FAILURE, json.type());
assertEquals(3600L, json.retryAfterSeconds());
assertNull(json.token());
}
@Test
void rateLimitFailureWithoutRetryAfterLeavesFieldNull() {
var result = new SendMessageResult(ADDRESS,
false,
false,
false,
false,
new RateLimitException(null),
null,
false);
var json = JsonSendMessageResult.from(result);
assertEquals(JsonSendMessageResult.Type.RATE_LIMIT_FAILURE, json.type());
assertNull(json.retryAfterSeconds());
}
@Test
void successLeavesRetryAfterNull() {
var result = new SendMessageResult(ADDRESS, true, false, false, false, null, null, false);
var json = JsonSendMessageResult.from(result);
assertEquals(JsonSendMessageResult.Type.SUCCESS, json.type());
assertNull(json.retryAfterSeconds());
}
@Test
void aggregateReturnsLongestRetryAfter() {
var small = rateLimited("+15551234567", 60L);
var big = rateLimited("+15559876543", 3600L);
var unknown = rateLimited("+15550000000", null);
var aggregate = new SendMessageResults(1L,
Map.of(new RecipientIdentifier.Uuid(UUID.randomUUID()), List.of(small, big, unknown)));
assertEquals(3600L, aggregate.maxRateLimitRetryAfterMilliseconds());
}
@Test
void aggregateReturnsNullWhenNoRetryAfter() {
var aggregate = new SendMessageResults(1L,
Map.of(new RecipientIdentifier.Uuid(UUID.randomUUID()), List.of(rateLimited("+15551234567", null))));
assertNull(aggregate.maxRateLimitRetryAfterMilliseconds());
}
/**
* Regression for a bug where the aggregate helper could overlook the longest
* wait if only some recipients reported a value. Ensures the max is picked
* across any mix which is what downstream captcha/rate-limit clients rely on.
*/
@Test
void aggregatePicksMaxEvenWhenSomeValuesAreNull() {
var withValue = rateLimited("+15551111111", 7200L);
var withoutValue = rateLimited("+15552222222", null);
var alsoWithValue = rateLimited("+15553333333", 120L);
var aggregate = new SendMessageResults(1L,
Map.of(new RecipientIdentifier.Uuid(UUID.randomUUID()),
List.of(withoutValue, withValue, alsoWithValue)));
assertEquals(7200L, aggregate.maxRateLimitRetryAfterMilliseconds());
}
private static SendMessageResult rateLimited(String number, Long retryAfterSeconds) {
return new SendMessageResult(new RecipientAddress(null, null, number, null),
false,
false,
false,
false,
new RateLimitException(retryAfterSeconds),
null,
false);
}
}