mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-06-16 18:01:04 +00:00
Compare commits
No commits in common. "master" and "v0.5.1" have entirely different histories.
57
.github/workflows/build.yml
vendored
57
.github/workflows/build.yml
vendored
@ -1,57 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
pull_request:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
# java="25" is the LTS Java version used in reproducible builds script (default in Containerfile).
|
|
||||||
# More Java versions can be added to test compatibility, eg. "26".
|
|
||||||
java: ["25", "26"]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
if [ "${{ matrix.java }}" != "25" ]; then
|
|
||||||
export OVERRIDE_JAVA_VERSION="${{ matrix.java }}"
|
|
||||||
fi
|
|
||||||
./reproducible-builds/build.sh
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: signal-cli-archive-${{ matrix.java }}
|
|
||||||
path: dist/*
|
|
||||||
|
|
||||||
build-client:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- ubuntu
|
|
||||||
- macos
|
|
||||||
- windows
|
|
||||||
runs-on: ${{ matrix.os }}-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./client
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Install rust
|
|
||||||
run: rustup default stable
|
|
||||||
- name: Build client
|
|
||||||
run: cargo build --release --verbose
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: signal-cli-client-${{ matrix.os }}
|
|
||||||
path: |
|
|
||||||
client/target/release/signal-cli-client
|
|
||||||
client/target/release/signal-cli-client.exe
|
|
||||||
60
.github/workflows/codeql-analysis.yml
vendored
60
.github/workflows/codeql-analysis.yml
vendored
@ -1,60 +0,0 @@
|
|||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ master ]
|
|
||||||
schedule:
|
|
||||||
- cron: '0 7 * * 4'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read # to fetch code (actions/checkout)
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyse:
|
|
||||||
name: Analyse
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Setup Java JDK
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: 25
|
|
||||||
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
# We must fetch at least the immediate parents so that if this is
|
|
||||||
# a pull request then we can checkout the head.
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v4
|
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
|
||||||
# with:
|
|
||||||
# languages: go, javascript, csharp, python, cpp, java
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v4
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v4
|
|
||||||
198
.github/workflows/release.yml
vendored
198
.github/workflows/release.yml
vendored
@ -1,198 +0,0 @@
|
|||||||
name: release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
env:
|
|
||||||
IMAGE_NAME: signal-cli
|
|
||||||
IMAGE_REGISTRY: ghcr.io/asamk
|
|
||||||
REGISTRY_USER: ${{ github.actor }}
|
|
||||||
REGISTRY_PASSWORD: ${{ github.token }}
|
|
||||||
ARCHIVE_JAVA_VERSION: 25
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
uses: ./.github/workflows/build.yml
|
|
||||||
|
|
||||||
release:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
outputs:
|
|
||||||
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: version
|
|
||||||
run: |
|
|
||||||
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
|
||||||
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
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
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.version.outputs.version }}.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.version.outputs.version }}.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload Linux native archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload Linux client archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
build-container:
|
|
||||||
needs: release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Download signal-cli build from CI workflow
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
|
|
||||||
- name: Move archive file
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Build Image
|
|
||||||
id: build_image
|
|
||||||
uses: redhat-actions/buildah-build@v2
|
|
||||||
with:
|
|
||||||
image: ${{ env.IMAGE_NAME }}
|
|
||||||
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
|
||||||
containerfiles: ./Containerfile
|
|
||||||
oci: true
|
|
||||||
|
|
||||||
- name: Push To GHCR
|
|
||||||
uses: redhat-actions/push-to-registry@v2
|
|
||||||
id: push
|
|
||||||
with:
|
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
|
||||||
tags: ${{ steps.build_image.outputs.tags }}
|
|
||||||
registry: ${{ env.IMAGE_REGISTRY }}
|
|
||||||
username: ${{ env.REGISTRY_USER }}
|
|
||||||
password: ${{ env.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Echo outputs
|
|
||||||
run: |
|
|
||||||
echo "${{ toJSON(steps.push.outputs) }}"
|
|
||||||
|
|
||||||
build-container-native:
|
|
||||||
needs: release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Download signal-cli build from CI workflow
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
|
|
||||||
- name: Move archive file
|
|
||||||
run: |
|
|
||||||
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
|
|
||||||
mkdir -p build/native/nativeCompile/
|
|
||||||
mv signal-cli build/native/nativeCompile/
|
|
||||||
chmod +x build/native/nativeCompile/signal-cli
|
|
||||||
|
|
||||||
- name: Build Image
|
|
||||||
id: build_image
|
|
||||||
uses: redhat-actions/buildah-build@v2
|
|
||||||
with:
|
|
||||||
image: ${{ env.IMAGE_NAME }}
|
|
||||||
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
|
||||||
containerfiles: ./native.Containerfile
|
|
||||||
oci: true
|
|
||||||
|
|
||||||
- name: Push To GHCR
|
|
||||||
uses: redhat-actions/push-to-registry@v2
|
|
||||||
id: push
|
|
||||||
with:
|
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
|
||||||
tags: ${{ steps.build_image.outputs.tags }}
|
|
||||||
registry: ${{ env.IMAGE_REGISTRY }}
|
|
||||||
username: ${{ env.REGISTRY_USER }}
|
|
||||||
password: ${{ env.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Echo outputs
|
|
||||||
run: |
|
|
||||||
echo "${{ toJSON(steps.push.outputs) }}"
|
|
||||||
|
|
||||||
build-container-client:
|
|
||||||
needs: release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Download signal-cli build from CI workflow
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
|
|
||||||
- name: Move archive file
|
|
||||||
run: |
|
|
||||||
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
|
|
||||||
mkdir -p client/target/release/
|
|
||||||
mv signal-cli-client client/target/release/
|
|
||||||
chmod +x client/target/release/signal-cli-client
|
|
||||||
|
|
||||||
- name: Build Image
|
|
||||||
id: build_image
|
|
||||||
uses: redhat-actions/buildah-build@v2
|
|
||||||
with:
|
|
||||||
image: ${{ env.IMAGE_NAME }}
|
|
||||||
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
|
||||||
containerfiles: ./client.Containerfile
|
|
||||||
oci: true
|
|
||||||
|
|
||||||
- name: Push To GHCR
|
|
||||||
uses: redhat-actions/push-to-registry@v2
|
|
||||||
id: push
|
|
||||||
with:
|
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
|
||||||
tags: ${{ steps.build_image.outputs.tags }}
|
|
||||||
registry: ${{ env.IMAGE_REGISTRY }}
|
|
||||||
username: ${{ env.REGISTRY_USER }}
|
|
||||||
password: ${{ env.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Echo outputs
|
|
||||||
run: |
|
|
||||||
echo "${{ toJSON(steps.push.outputs) }}"
|
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
@ -1,22 +1,7 @@
|
|||||||
.gradle/
|
.gradle/
|
||||||
.kotlin/
|
.idea/
|
||||||
.idea/*
|
|
||||||
!.idea/codeStyles/
|
|
||||||
build/
|
build/
|
||||||
*~
|
*~
|
||||||
*.swp
|
*.swp
|
||||||
*.iml
|
*.iml
|
||||||
local.properties
|
local.properties
|
||||||
.classpath
|
|
||||||
.project
|
|
||||||
.settings/
|
|
||||||
out/
|
|
||||||
.DS_Store
|
|
||||||
/bin/
|
|
||||||
/test-config/
|
|
||||||
/dist/
|
|
||||||
/github/
|
|
||||||
man/*.1
|
|
||||||
man/*.5
|
|
||||||
man/man1
|
|
||||||
man/man5
|
|
||||||
|
|||||||
71
.idea/codeStyles/Project.xml
generated
71
.idea/codeStyles/Project.xml
generated
@ -1,71 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<code_scheme name="Project" version="173">
|
|
||||||
<option name="LINE_SEPARATOR" value=" " />
|
|
||||||
<JavaCodeStyleSettings>
|
|
||||||
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
|
||||||
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
|
|
||||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
|
||||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
|
||||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
|
||||||
<option name="IMPORT_LAYOUT_TABLE">
|
|
||||||
<value>
|
|
||||||
<package name="com" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="junit" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="net" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="org" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="java" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="javax" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="true" />
|
|
||||||
<emptyLine />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="RECORD_COMPONENTS_WRAP" value="5" />
|
|
||||||
<option name="NEW_LINE_AFTER_LPAREN_IN_RECORD_HEADER" value="true" />
|
|
||||||
<option name="RPAREN_ON_NEW_LINE_IN_RECORD_HEADER" value="true" />
|
|
||||||
<option name="JD_P_AT_EMPTY_LINES" value="false" />
|
|
||||||
</JavaCodeStyleSettings>
|
|
||||||
<JetCodeStyleSettings>
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</JetCodeStyleSettings>
|
|
||||||
<codeStyleSettings language="JAVA">
|
|
||||||
<option name="RIGHT_MARGIN" value="120" />
|
|
||||||
<option name="KEEP_LINE_BREAKS" value="false" />
|
|
||||||
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
|
||||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
|
||||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
|
||||||
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
|
|
||||||
<option name="CALL_PARAMETERS_WRAP" value="5" />
|
|
||||||
<option name="METHOD_PARAMETERS_WRAP" value="5" />
|
|
||||||
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
|
|
||||||
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
|
|
||||||
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
|
|
||||||
<option name="PARENTHESES_EXPRESSION_LPAREN_WRAP" value="true" />
|
|
||||||
<option name="PARENTHESES_EXPRESSION_RPAREN_WRAP" value="true" />
|
|
||||||
<option name="BINARY_OPERATION_WRAP" value="5" />
|
|
||||||
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
|
||||||
<option name="TERNARY_OPERATION_WRAP" value="5" />
|
|
||||||
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
|
||||||
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
|
|
||||||
<option name="ARRAY_INITIALIZER_WRAP" value="5" />
|
|
||||||
<option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
|
|
||||||
<option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" />
|
|
||||||
<option name="ENUM_CONSTANTS_WRAP" value="2" />
|
|
||||||
</codeStyleSettings>
|
|
||||||
<codeStyleSettings language="XML">
|
|
||||||
<arrangement>
|
|
||||||
<rules />
|
|
||||||
</arrangement>
|
|
||||||
</codeStyleSettings>
|
|
||||||
<codeStyleSettings language="kotlin">
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</codeStyleSettings>
|
|
||||||
</code_scheme>
|
|
||||||
</component>
|
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@ -1,5 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<state>
|
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
||||||
1368
CHANGELOG.md
1368
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -1,18 +0,0 @@
|
|||||||
# Question
|
|
||||||
|
|
||||||
If you have a question you can ask it in the [GitHub discussions page](https://github.com/AsamK/signal-cli/discussions)
|
|
||||||
|
|
||||||
# Report a bug
|
|
||||||
|
|
||||||
- Search [existing issues](https://github.com/AsamK/signal-cli/issues?q=is%3Aissue) if it has been reported already
|
|
||||||
- If you're unable to find an open issue addressing the
|
|
||||||
problem, [open a new one](https://github.com/AsamK/signal-cli/issues/new).
|
|
||||||
- Be sure to include a **title and clear description**, as much relevant information as possible.
|
|
||||||
- Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using
|
|
||||||
- Specify if it's the normal java or the graalvm native version.
|
|
||||||
- Run the failing command with `-vv --scrub-log` flags to get a more detailed log output and include that in the bug report
|
|
||||||
|
|
||||||
# Pull request
|
|
||||||
|
|
||||||
- Code style should match the existing code, IntelliJ users can use the auto formatter
|
|
||||||
- Separate PRs should be opened for each implemented feature or bug fix
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
FROM docker.io/azul/zulu-openjdk:25-jre-headless
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
|
|
||||||
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
|
|
||||||
LABEL org.opencontainers.image.licenses=GPL-3.0-only
|
|
||||||
|
|
||||||
RUN useradd signal-cli --system --create-home --home-dir /var/lib/signal-cli
|
|
||||||
ADD build/install/signal-cli /opt/signal-cli
|
|
||||||
|
|
||||||
USER signal-cli
|
|
||||||
ENTRYPOINT ["/opt/signal-cli/bin/signal-cli", "--config=/var/lib/signal-cli"]
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
github: AsamK
|
|
||||||
liberapay: asamk
|
|
||||||
ko_fi: asamk
|
|
||||||
#bitcoin: bc1qykae53fry8a8ycgdzgv0rlxfc959hmmllvz698
|
|
||||||
250
README.md
250
README.md
@ -1,178 +1,174 @@
|
|||||||
# signal-cli
|
# signal-cli
|
||||||
|
|
||||||
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. To be able to receive messages signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libsignal-service-java/pull/5) nor [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls.
|
||||||
It supports registering, verifying, sending and receiving messages.
|
It is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings.
|
||||||
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
|
||||||
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal-service).
|
|
||||||
For registering you need a phone number where you can receive SMS or incoming calls.
|
|
||||||
|
|
||||||
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
|
||||||
For this use-case, it has a daemon mode with JSON-RPC interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.5.adoc))
|
|
||||||
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)).
|
|
||||||
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
|
|
||||||
|
|
||||||
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
|
|
||||||
The official Signal clients expire after three months and then the Signal-Server can make incompatible changes.
|
|
||||||
So signal-cli releases older than three months may not work correctly.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
You can [build signal-cli](#building) yourself or use
|
You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/). You need to have at least JRE 7 installed, to run signal-cli.
|
||||||
the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and
|
|
||||||
Windows. There's also a [docker image and some Linux packages](https://github.com/AsamK/signal-cli/wiki/Binary-distributions) provided by the community.
|
|
||||||
|
|
||||||
System requirements:
|
|
||||||
|
|
||||||
- at least Java Runtime Environment (JRE) 25
|
|
||||||
- native library: libsignal-client
|
|
||||||
|
|
||||||
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
|
|
||||||
systems/architectures
|
|
||||||
see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
|
|
||||||
|
|
||||||
### Install system-wide on Linux [ JVM build ]
|
|
||||||
|
|
||||||
|
### Install system-wide on Linux
|
||||||
See [latest version](https://github.com/AsamK/signal-cli/releases).
|
See [latest version](https://github.com/AsamK/signal-cli/releases).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
|
export VERSION=<latest version, format "x.y.z">
|
||||||
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz
|
wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz
|
||||||
sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt
|
sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt
|
||||||
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
|
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install system-wide on Linux [ GraalVM native build ]
|
|
||||||
|
|
||||||
```sh
|
|
||||||
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
|
|
||||||
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux-native.tar.gz
|
|
||||||
sudo tar xf signal-cli-"${VERSION}"-Linux-native.tar.gz -C /opt
|
|
||||||
sudo ln -sf /opt/signal-cli /usr/local/bin/
|
|
||||||
```
|
|
||||||
|
|
||||||
You can find further instructions on the Wiki:
|
|
||||||
|
|
||||||
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
For a complete usage overview please read
|
usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-system] {link,addDevice,listDevices,removeDevice,register,verify,send,quitGroup,updateGroup,listIdentities,trust,receive,daemon} ...
|
||||||
the [man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.adoc) and
|
|
||||||
the [wiki](https://github.com/AsamK/signal-cli/wiki).
|
|
||||||
|
|
||||||
Important: The ACCOUNT is your phone number in international format and must include the country calling code. Hence it
|
|
||||||
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
|
|
||||||
of all country codes.)
|
|
||||||
|
|
||||||
* Register a number (with SMS verification)
|
* Register a number (with SMS verification)
|
||||||
|
|
||||||
signal-cli -a ACCOUNT register
|
signal-cli -u USERNAME register
|
||||||
|
|
||||||
You can register Signal using a landline number. In this case, you need to follow the procedure below:
|
* Register a number (with voice verification)
|
||||||
* Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`)
|
|
||||||
* You will get an error `400 (InvalidTransportModeException)`, this is normal
|
|
||||||
* Wait 60 seconds
|
|
||||||
* Attempt a voice call verification by adding the `--voice` switch and wait for the call:
|
|
||||||
|
|
||||||
```sh
|
signal-cli -u USERNAME register -v
|
||||||
signal-cli -a ACCOUNT register --voice
|
|
||||||
```
|
|
||||||
|
|
||||||
Registering may require solving a CAPTCHA
|
* Verify the number using the code received via SMS or voice
|
||||||
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
|
|
||||||
|
|
||||||
* Verify the number using the code received via SMS or voice, optionally add `--pin PIN_CODE` if you've added a pin code
|
signal-cli -u USERNAME verify CODE
|
||||||
to your account
|
|
||||||
|
|
||||||
signal-cli -a ACCOUNT verify CODE
|
* Send a message to one or more recipients
|
||||||
|
|
||||||
* Send a message
|
signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]]
|
||||||
|
|
||||||
```sh
|
|
||||||
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
|
|
||||||
```
|
|
||||||
|
|
||||||
* Send a message to a username, usernames need to be prefixed with `u:`
|
|
||||||
|
|
||||||
```sh
|
|
||||||
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
|
|
||||||
```
|
|
||||||
|
|
||||||
* Pipe the message content from another process.
|
* Pipe the message content from another process.
|
||||||
|
|
||||||
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
|
uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]]
|
||||||
|
|
||||||
* Receive messages
|
* Receive messages
|
||||||
|
|
||||||
signal-cli -a ACCOUNT receive
|
signal-cli -u USERNAME receive
|
||||||
|
|
||||||
**Hint**: The Signal protocol expects that incoming messages are regularly received (using `daemon` or `receive`
|
* Groups
|
||||||
command). This is required for the encryption to work efficiently and for getting updates to groups, expiration timer
|
|
||||||
and other features.
|
* Create a group
|
||||||
|
|
||||||
|
signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]]
|
||||||
|
|
||||||
|
* Update a group
|
||||||
|
|
||||||
|
signal-cli -u USERNAME updateGroup -g GROUP_ID -n "New group name" -a "AVATAR_IMAGE_FILE"
|
||||||
|
|
||||||
|
* Add member to a group
|
||||||
|
|
||||||
|
signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER"
|
||||||
|
|
||||||
|
* Leave a group
|
||||||
|
|
||||||
|
signal-cli -u USERNAME quitGroup -g GROUP_ID
|
||||||
|
|
||||||
|
* Send a message to a group
|
||||||
|
|
||||||
|
signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID
|
||||||
|
|
||||||
|
* Linking other devices (Provisioning)
|
||||||
|
|
||||||
|
* Connect to another device
|
||||||
|
|
||||||
|
signal-cli link -n "optional device name"
|
||||||
|
|
||||||
|
This shows a "tsdevice:/…" link, if you want to connect to another signal-cli instance, you can just use this link. If you want to link to and Android device, create a QR code with the link (e.g. with [qrencode](https://fukuchi.org/works/qrencode/)) and scan that in the Signal Android app.
|
||||||
|
|
||||||
|
* Add another device
|
||||||
|
|
||||||
|
signal-cli -u USERNAME addDevice --uri "tsdevice:/…"
|
||||||
|
|
||||||
|
The "tsdevice:/…" link is the one shown by the new signal-cli instance or contained in the QR code shown in Signal-Desktop or similar apps.
|
||||||
|
Only the master device (that was registered directly, not linked) can add new devices.
|
||||||
|
|
||||||
|
* Manage linked devices
|
||||||
|
|
||||||
|
signal-cli -u USERNAME listDevices
|
||||||
|
|
||||||
|
signal-cli -u USERNAME removeDevice -d DEVICE_ID
|
||||||
|
|
||||||
|
* Manage trusted keys
|
||||||
|
|
||||||
|
* View all known keys
|
||||||
|
|
||||||
|
signal-cli -u USERNAME listIdentities
|
||||||
|
|
||||||
|
* View known keys of one number
|
||||||
|
|
||||||
|
signal-cli -u USERNAME listIdentities -n NUMBER
|
||||||
|
|
||||||
|
* Trust new key, after having verified it
|
||||||
|
|
||||||
|
signal-cli -u USERNAME trust -v FINGER_PRINT NUMBER
|
||||||
|
|
||||||
|
* Trust new key, without having verified it. Only use this if you don't care about security
|
||||||
|
|
||||||
|
signal-cli -u USERNAME trust -a NUMBER
|
||||||
|
|
||||||
|
## DBus service
|
||||||
|
|
||||||
|
signal-cli can run in daemon mode and provides an experimental dbus interface.
|
||||||
|
For dbus support you need jni/unix-java.so installed on your system (Debian: libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)).
|
||||||
|
|
||||||
|
* Run in daemon mode (dbus session bus)
|
||||||
|
|
||||||
|
signal-cli -u USERNAME daemon
|
||||||
|
|
||||||
|
* Send a message via dbus
|
||||||
|
|
||||||
|
signal-cli --dbus send -m "Message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]]
|
||||||
|
|
||||||
|
### System bus
|
||||||
|
|
||||||
|
To run on the system bus you need to take some additional steps.
|
||||||
|
It’s advisable to run signal-cli as a separate unix user, the following steps assume you created a user named *signal-cli*.
|
||||||
|
These steps, executed as root, should work on all distributions using systemd.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp data/org.asamk.Signal.conf /etc/dbus-1/system.d/
|
||||||
|
cp data/org.asamk.Signal.service /usr/share/dbus-1/system-services/
|
||||||
|
cp data/signal.service /etc/systemd/system/
|
||||||
|
sed -i -e "s|%dir%|<INSERT_INSTALL_PATH>|" -e "s|%number%|<INSERT_YOUR_NUMBER>|" /etc/systemd/system/signal.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable signal.service
|
||||||
|
systemctl reload dbus.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Then just execute the send command from above, the service will be autostarted by dbus the first time it is requested.
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
The password and cryptographic keys are created when registering and stored in the current users home directory:
|
The password and cryptographic keys are created when registering and stored in the current users home directory:
|
||||||
|
|
||||||
$XDG_DATA_HOME/signal-cli/data/
|
$HOME/.config/signal/data/
|
||||||
$HOME/.local/share/signal-cli/data/
|
|
||||||
|
For legacy users, the old config directory is used as a fallback:
|
||||||
|
|
||||||
|
$HOME/.config/textsecure/data/
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
This project uses [Gradle](http://gradle.org) for building and maintaining dependencies. If you have a recent gradle
|
This project uses [Gradle](http://gradle.org) for building and maintaining
|
||||||
version installed, you can replace `./gradlew` with `gradle` in the following steps.
|
dependencies. If you have a recent gradle version installed, you can replace `./gradlew` with `gradle` in the following steps.
|
||||||
|
|
||||||
1. Checkout the source somewhere on your filesystem with
|
1. Checkout the source somewhere on your filesystem with
|
||||||
|
|
||||||
git clone https://github.com/AsamK/signal-cli.git
|
git clone https://github.com/AsamK/signal-cli.git
|
||||||
|
|
||||||
2. Execute Gradle:
|
2. Execute Gradle:
|
||||||
|
|
||||||
./gradlew build
|
./gradlew build
|
||||||
|
|
||||||
2a. Create shell wrapper in *build/install/signal-cli/bin*:
|
3. Create shell wrapper in *build/install/signal-cli/bin*:
|
||||||
|
|
||||||
./gradlew installDist
|
./gradlew installDist
|
||||||
|
|
||||||
2b. Create tar file in *build/distributions*:
|
4. Create tar file in *build/distributions*:
|
||||||
|
|
||||||
./gradlew distTar
|
./gradlew distTar
|
||||||
|
|
||||||
2c. Create a fat tar file in *build/libs/signal-cli-fat*:
|
## Troubleshooting
|
||||||
|
If you use a version of the Oracle JRE and get an InvalidKeyException you need to enable unlimited strength crypto. See https://stackoverflow.com/questions/6481627/java-security-illegal-key-size-or-default-parameters for instructions.
|
||||||
./gradlew fatJar
|
|
||||||
|
|
||||||
2d. Compile and run signal-cli:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
./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
|
|
||||||
work in all situations.
|
|
||||||
|
|
||||||
1. [Install GraalVM and setup the environment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
|
|
||||||
2. Execute Gradle:
|
|
||||||
|
|
||||||
./gradlew nativeCompile
|
|
||||||
|
|
||||||
The binary is available at *build/native/nativeCompile/signal-cli*
|
|
||||||
|
|
||||||
## FAQ and Troubleshooting
|
|
||||||
|
|
||||||
For frequently asked questions and issues have a look at the [wiki](https://github.com/AsamK/signal-cli/wiki/FAQ).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
35
build.gradle
Normal file
35
build.gradle
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_7
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_7
|
||||||
|
|
||||||
|
mainClassName = 'org.asamk.signal.Main'
|
||||||
|
|
||||||
|
version = '0.5.1'
|
||||||
|
|
||||||
|
compileJava.options.encoding = 'UTF-8'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url "https://raw.github.com/AsamK/maven/master/releases/"
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile 'com.github.turasa:signal-service-java:2.4.0_unofficial_1'
|
||||||
|
compile 'org.bouncycastle:bcprov-jdk15on:1.55'
|
||||||
|
compile 'net.sourceforge.argparse4j:argparse4j:0.7.0'
|
||||||
|
compile 'org.freedesktop.dbus:dbus-java:2.7.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Implementation-Title': project.name,
|
||||||
|
'Implementation-Version': project.version,
|
||||||
|
'Main-Class': project.mainClassName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
202
build.gradle.kts
202
build.gradle.kts
@ -1,202 +0,0 @@
|
|||||||
import groovy.json.JsonOutput
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
java
|
|
||||||
application
|
|
||||||
eclipse
|
|
||||||
`check-lib-versions`
|
|
||||||
id("org.graalvm.buildtools.native") version "1.1.2"
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
group = "org.asamk"
|
|
||||||
version = "0.14.5"
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_25
|
|
||||||
targetCompatibility = JavaVersion.VERSION_25
|
|
||||||
|
|
||||||
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
|
||||||
toolchain {
|
|
||||||
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
|
||||||
mainClass.set("org.asamk.signal.Main")
|
|
||||||
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
|
|
||||||
}
|
|
||||||
|
|
||||||
graalvmNative {
|
|
||||||
binaries {
|
|
||||||
this["main"].run {
|
|
||||||
buildArgs.add("-Dfile.encoding=UTF-8")
|
|
||||||
buildArgs.add("-J-Dfile.encoding=UTF-8")
|
|
||||||
buildArgs.add("-march=compatibility")
|
|
||||||
buildArgs.add("--enable-native-access=ALL-UNNAMED")
|
|
||||||
resources.autodetect()
|
|
||||||
if (System.getenv("GRAALVM_HOME") == null) {
|
|
||||||
toolchainDetection.set(true)
|
|
||||||
javaLauncher.set(javaToolchains.launcherFor {
|
|
||||||
languageVersion.set(JavaLanguageVersion.of(25))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
toolchainDetection.set(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val artifactType = Attribute.of("artifactType", String::class.java)
|
|
||||||
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
|
|
||||||
dependencies {
|
|
||||||
attributesSchema {
|
|
||||||
attribute(minified)
|
|
||||||
}
|
|
||||||
artifactTypes.getByName("jar") {
|
|
||||||
attributes.attribute(minified, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations.runtimeClasspath.configure {
|
|
||||||
attributes {
|
|
||||||
attribute(minified, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val excludePatterns = mapOf(
|
|
||||||
"libsignal-client" to setOf(
|
|
||||||
"libsignal_jni_testing_amd64.so",
|
|
||||||
"signal_jni_testing_amd64.dll",
|
|
||||||
"libsignal_jni_testing_amd64.dylib",
|
|
||||||
"libsignal_jni_testing_aarch64.dylib",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val schemaAnnotationProcessor by configurations.creating {
|
|
||||||
isCanBeConsumed = false
|
|
||||||
isCanBeResolved = true
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
registerTransform(JarFileExcluder::class) {
|
|
||||||
from.attribute(minified, false).attribute(artifactType, "jar")
|
|
||||||
to.attribute(minified, true).attribute(artifactType, "jar")
|
|
||||||
|
|
||||||
parameters {
|
|
||||||
excludeFilesByArtifact = excludePatterns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schemaAnnotationProcessor(libs.micronaut.json.schema.processor)
|
|
||||||
schemaAnnotationProcessor(libs.micronaut.inject.java)
|
|
||||||
implementation(libs.bouncycastle)
|
|
||||||
implementation(libs.jackson.databind)
|
|
||||||
implementation(libs.argparse4j)
|
|
||||||
implementation(libs.dbusjava)
|
|
||||||
implementation(libs.slf4j.api)
|
|
||||||
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)
|
|
||||||
testImplementation(platform(libs.junit.jupiter.bom))
|
|
||||||
testRuntimeOnly(libs.junit.launcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named<Test>("test") {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
implementation {
|
|
||||||
resolutionStrategy.failOnVersionConflict()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
tasks.withType<AbstractArchiveTask>().configureEach {
|
|
||||||
isPreserveFileTimestamps = false
|
|
||||||
isReproducibleFileOrder = true
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<JavaCompile> {
|
|
||||||
options.encoding = "UTF-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<Jar> {
|
|
||||||
manifest {
|
|
||||||
attributes(
|
|
||||||
"Implementation-Title" to project.name,
|
|
||||||
"Implementation-Version" to project.version,
|
|
||||||
"Main-Class" to application.mainClass.get(),
|
|
||||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("fatJar", type = Jar::class) {
|
|
||||||
archiveBaseName.set("${project.name}-fat")
|
|
||||||
exclude(
|
|
||||||
"META-INF/*.SF",
|
|
||||||
"META-INF/**/*.MF",
|
|
||||||
"META-INF/*.DSA",
|
|
||||||
"META-INF/*.RSA",
|
|
||||||
"META-INF/NOTICE*",
|
|
||||||
"META-INF/LICENSE*",
|
|
||||||
"META-INF/INDEX.LIST",
|
|
||||||
"**/module-info.class",
|
|
||||||
)
|
|
||||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
|
||||||
doFirst {
|
|
||||||
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
|
|
||||||
}
|
|
||||||
with(tasks.jar.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("writeLibsignalVersion") {
|
|
||||||
doLast {
|
|
||||||
val resolutionResult = configurations.runtimeClasspath.get().incoming.resolutionResult
|
|
||||||
val libsignalDep =
|
|
||||||
resolutionResult.allDependencies.find { dep -> dep.requested is ModuleComponentSelector && (dep.requested as ModuleComponentSelector).group == "org.signal" && (dep.requested as ModuleComponentSelector).moduleIdentifier.name == "libsignal-client" }
|
|
||||||
if (libsignalDep != null) {
|
|
||||||
val version = (libsignalDep.requested as ModuleComponentSelector).version
|
|
||||||
file("libsignal-version").writeText(version + "\n")
|
|
||||||
} else {
|
|
||||||
throw GradleException("Could not find libsignal-client dependency")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
`kotlin-dsl`
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
|
|
||||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_25)
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
targetCompatibility = JavaVersion.VERSION_25
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
gradlePlugin {
|
|
||||||
plugins {
|
|
||||||
register("check-lib-versions") {
|
|
||||||
id = "check-lib-versions"
|
|
||||||
implementationClass = "CheckLibVersionsPlugin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import org.gradle.api.Plugin
|
|
||||||
import org.gradle.api.Project
|
|
||||||
import org.gradle.api.Task
|
|
||||||
import org.gradle.api.artifacts.Dependency
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
|
||||||
|
|
||||||
class CheckLibVersionsPlugin : Plugin<Project> {
|
|
||||||
override fun apply(project: Project) {
|
|
||||||
project.tasks.register("checkLibVersions") {
|
|
||||||
description =
|
|
||||||
"Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded."
|
|
||||||
doLast {
|
|
||||||
project.configurations.flatMap { it.allDependencies }
|
|
||||||
.toSet()
|
|
||||||
.forEach { checkDependency(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Task.checkDependency(dependency: Dependency) {
|
|
||||||
val version = dependency.version
|
|
||||||
val group = dependency.group
|
|
||||||
val path = group?.replace(".", "/") ?: ""
|
|
||||||
val name = dependency.name
|
|
||||||
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
|
|
||||||
try {
|
|
||||||
val dbf = DocumentBuilderFactory.newInstance()
|
|
||||||
val db = dbf.newDocumentBuilder()
|
|
||||||
val doc = db.parse(metaDataUrl)
|
|
||||||
val newest = doc.getElementsByTagName("latest").item(0).textContent
|
|
||||||
if (version != newest.toString()) {
|
|
||||||
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
logger.debug("Unable to download or parse {}: {}", metaDataUrl, e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import org.gradle.api.artifacts.transform.*
|
|
||||||
import org.gradle.api.file.FileSystemLocation
|
|
||||||
import org.gradle.api.provider.Provider
|
|
||||||
import org.gradle.api.tasks.Input
|
|
||||||
import org.gradle.api.tasks.PathSensitive
|
|
||||||
import org.gradle.api.tasks.PathSensitivity
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
@CacheableTransform
|
|
||||||
abstract class JarFileExcluder : TransformAction<JarFileExcluder.Parameters> {
|
|
||||||
interface Parameters : TransformParameters {
|
|
||||||
@get:Input
|
|
||||||
var excludeFilesByArtifact: Map<String, Set<String>>
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:PathSensitive(PathSensitivity.NAME_ONLY)
|
|
||||||
@get:InputArtifact
|
|
||||||
abstract val inputArtifact: Provider<FileSystemLocation>
|
|
||||||
|
|
||||||
override
|
|
||||||
fun transform(outputs: TransformOutputs) {
|
|
||||||
val fileName = inputArtifact.get().asFile.name
|
|
||||||
for (entry in parameters.excludeFilesByArtifact) {
|
|
||||||
if (fileName.startsWith(entry.key)) {
|
|
||||||
val nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf("."))
|
|
||||||
excludeFiles(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}.jar"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outputs.file(inputArtifact)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun excludeFiles(artifact: File, excludeFiles: Set<String>, jarFile: File) {
|
|
||||||
ZipInputStream(FileInputStream(artifact)).use { input ->
|
|
||||||
ZipOutputStream(FileOutputStream(jarFile)).use { output ->
|
|
||||||
var entry = input.nextEntry
|
|
||||||
while (entry != null) {
|
|
||||||
if (!excludeFiles.contains(entry.name)) {
|
|
||||||
output.putNextEntry(entry)
|
|
||||||
input.copyTo(output)
|
|
||||||
output.closeEntry()
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = input.nextEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
FROM docker.io/debian:testing-slim
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
|
|
||||||
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
|
|
||||||
LABEL org.opencontainers.image.licenses=GPL-3.0-only
|
|
||||||
|
|
||||||
RUN useradd signal-cli --system
|
|
||||||
ADD client/target/release/signal-cli-client /usr/bin/signal-cli-client
|
|
||||||
|
|
||||||
USER signal-cli
|
|
||||||
ENTRYPOINT ["/usr/bin/signal-cli-client"]
|
|
||||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/target/
|
|
||||||
1664
client/Cargo.lock
generated
1664
client/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,22 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "signal-cli-client"
|
|
||||||
version = "0.0.1"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1"
|
|
||||||
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
|
|
||||||
serde = "1"
|
|
||||||
serde_json = "1"
|
|
||||||
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
|
|
||||||
jsonrpsee = { version = "0.26", features = [
|
|
||||||
"macros",
|
|
||||||
"async-client",
|
|
||||||
"http-client",
|
|
||||||
] }
|
|
||||||
bytes = "1"
|
|
||||||
tokio-util = "0.7"
|
|
||||||
futures-util = "0.3"
|
|
||||||
thiserror = "2"
|
|
||||||
@ -1,669 +0,0 @@
|
|||||||
use std::{ffi::OsString, net::SocketAddr};
|
|
||||||
|
|
||||||
use clap::{crate_version, Parser, Subcommand, ValueEnum};
|
|
||||||
|
|
||||||
/// JSON-RPC client for signal-cli
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(rename_all = "kebab-case", version = crate_version!())]
|
|
||||||
pub struct Cli {
|
|
||||||
/// Account to use (for daemon in multi-account mode)
|
|
||||||
#[arg(short = 'a', long)]
|
|
||||||
pub account: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
pub output: Option<String>,
|
|
||||||
|
|
||||||
/// TCP host and port of signal-cli daemon
|
|
||||||
#[arg(long, conflicts_with = "json_rpc_http")]
|
|
||||||
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
|
||||||
|
|
||||||
/// UNIX socket address and port of signal-cli daemon
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
|
||||||
pub json_rpc_socket: Option<Option<OsString>>,
|
|
||||||
|
|
||||||
/// HTTP URL of signal-cli daemon
|
|
||||||
#[arg(long, conflicts_with = "json_rpc_socket")]
|
|
||||||
pub json_rpc_http: Option<Option<String>>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
pub verbose: bool,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: CliCommands,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Subcommand, Debug)]
|
|
||||||
#[command(rename_all = "camelCase", version = crate_version!())]
|
|
||||||
pub enum CliCommands {
|
|
||||||
AddDevice {
|
|
||||||
#[arg(long)]
|
|
||||||
uri: String,
|
|
||||||
},
|
|
||||||
AddStickerPack {
|
|
||||||
#[arg(long)]
|
|
||||||
uri: String,
|
|
||||||
},
|
|
||||||
#[command(rename_all = "kebab-case")]
|
|
||||||
Block {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long)]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
},
|
|
||||||
DeleteLocalAccountData {
|
|
||||||
#[arg(long = "ignore-registered")]
|
|
||||||
ignore_registered: Option<bool>,
|
|
||||||
},
|
|
||||||
FinishChangeNumber {
|
|
||||||
number: String,
|
|
||||||
#[arg(short = 'v', long = "verification-code")]
|
|
||||||
verification_code: String,
|
|
||||||
|
|
||||||
#[arg(short = 'p', long)]
|
|
||||||
pin: Option<String>,
|
|
||||||
},
|
|
||||||
GetAttachment {
|
|
||||||
#[arg(long)]
|
|
||||||
id: String,
|
|
||||||
#[arg(long)]
|
|
||||||
recipient: Option<String>,
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Option<String>,
|
|
||||||
},
|
|
||||||
GetAvatar {
|
|
||||||
#[arg(long)]
|
|
||||||
contact: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
profile: Option<String>,
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Option<String>,
|
|
||||||
},
|
|
||||||
GetSticker {
|
|
||||||
#[arg(long = "pack-id")]
|
|
||||||
pack_id: String,
|
|
||||||
#[arg(long = "sticker-id")]
|
|
||||||
sticker_id: u32,
|
|
||||||
},
|
|
||||||
GetUserStatus {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
username: Vec<String>,
|
|
||||||
},
|
|
||||||
JoinGroup {
|
|
||||||
#[arg(long)]
|
|
||||||
uri: String,
|
|
||||||
},
|
|
||||||
Link {
|
|
||||||
#[arg(short = 'n', long)]
|
|
||||||
name: Option<String>,
|
|
||||||
},
|
|
||||||
ListAccounts,
|
|
||||||
ListContacts {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
#[arg(short = 'a', long = "all-recipients")]
|
|
||||||
all_recipients: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
blocked: Option<bool>,
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
detailed: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
internal: bool,
|
|
||||||
},
|
|
||||||
ListDevices,
|
|
||||||
ListGroups {
|
|
||||||
#[arg(short = 'd', long)]
|
|
||||||
detailed: bool,
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
},
|
|
||||||
ListIdentities {
|
|
||||||
#[arg(short = 'n', long)]
|
|
||||||
number: Option<String>,
|
|
||||||
},
|
|
||||||
ListStickerPacks,
|
|
||||||
QuitGroup {
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: String,
|
|
||||||
#[arg(long)]
|
|
||||||
delete: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
admin: Vec<String>,
|
|
||||||
},
|
|
||||||
Receive {
|
|
||||||
#[arg(short = 't', long, default_value_t = 3.0)]
|
|
||||||
timeout: f64,
|
|
||||||
},
|
|
||||||
Register {
|
|
||||||
#[arg(short = 'v', long)]
|
|
||||||
voice: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
captcha: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
reregister: bool,
|
|
||||||
},
|
|
||||||
RemoveContact {
|
|
||||||
recipient: String,
|
|
||||||
#[arg(long)]
|
|
||||||
forget: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
hide: bool,
|
|
||||||
},
|
|
||||||
RemoveDevice {
|
|
||||||
#[arg(short = 'd', long = "device-id")]
|
|
||||||
device_id: u32,
|
|
||||||
},
|
|
||||||
RemovePin,
|
|
||||||
RemoteDelete {
|
|
||||||
#[arg(short = 't', long = "target-timestamp")]
|
|
||||||
target_timestamp: u64,
|
|
||||||
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
|
||||||
note_to_self: bool,
|
|
||||||
},
|
|
||||||
#[command(rename_all = "kebab-case")]
|
|
||||||
Send {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long)]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
note_to_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
notify_self: bool,
|
|
||||||
|
|
||||||
#[arg(short = 'e', long)]
|
|
||||||
end_session: bool,
|
|
||||||
|
|
||||||
#[arg(short = 'm', long)]
|
|
||||||
message: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
message_from_stdin: bool,
|
|
||||||
|
|
||||||
#[arg(short = 'a', long)]
|
|
||||||
attachment: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
view_once: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
mention: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
text_style: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
quote_timestamp: Option<u64>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
quote_author: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
quote_message: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
quote_mention: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
quote_text_style: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
quote_attachment: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
preview_url: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
preview_title: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
preview_description: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
preview_image: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
sticker: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
story_timestamp: Option<u64>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
story_author: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
edit_timestamp: Option<u64>,
|
|
||||||
|
|
||||||
#[arg(long = "no-urgent")]
|
|
||||||
no_urgent: bool,
|
|
||||||
},
|
|
||||||
SendAdminDelete {
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'a', long = "target-author")]
|
|
||||||
target_author: String,
|
|
||||||
|
|
||||||
#[arg(short = 't', long = "target-timestamp")]
|
|
||||||
target_timestamp: u64,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
story: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
notify_self: bool,
|
|
||||||
},
|
|
||||||
SendContacts,
|
|
||||||
SendPaymentNotification {
|
|
||||||
recipient: String,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
receipt: String,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
note: String,
|
|
||||||
},
|
|
||||||
SendPinMessage {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'a', long = "target-author")]
|
|
||||||
target_author: String,
|
|
||||||
|
|
||||||
#[arg(short = 't', long = "target-timestamp")]
|
|
||||||
target_timestamp: u64,
|
|
||||||
|
|
||||||
#[arg(short = 'd', long = "pin-duration")]
|
|
||||||
pin_duration: Option<i32>,
|
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
|
||||||
note_to_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
notify_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
story: bool,
|
|
||||||
},
|
|
||||||
SendPollCreate {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'q', long = "question")]
|
|
||||||
question: String,
|
|
||||||
|
|
||||||
#[arg(short = 'o', long = "option")]
|
|
||||||
option: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long = "no-multi")]
|
|
||||||
no_multi: bool,
|
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
|
||||||
note_to_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
notify_self: bool,
|
|
||||||
},
|
|
||||||
SendPollTerminate {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long = "poll-timestamp")]
|
|
||||||
poll_timestamp: u64,
|
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
|
||||||
note_to_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
notify_self: bool,
|
|
||||||
},
|
|
||||||
SendPollVote {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long = "poll-author")]
|
|
||||||
poll_author: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "poll-timestamp")]
|
|
||||||
poll_timestamp: u64,
|
|
||||||
|
|
||||||
#[arg(short = 'o', long = "option")]
|
|
||||||
option: Vec<i32>,
|
|
||||||
|
|
||||||
#[arg(long = "vote-count")]
|
|
||||||
vote_count: i32,
|
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
|
||||||
note_to_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
notify_self: bool,
|
|
||||||
},
|
|
||||||
SendReaction {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
|
||||||
note_to_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
notify_self: bool,
|
|
||||||
|
|
||||||
#[arg(short = 'e', long)]
|
|
||||||
emoji: String,
|
|
||||||
|
|
||||||
#[arg(short = 'a', long = "target-author")]
|
|
||||||
target_author: String,
|
|
||||||
|
|
||||||
#[arg(short = 't', long = "target-timestamp")]
|
|
||||||
target_timestamp: u64,
|
|
||||||
|
|
||||||
#[arg(short = 'r', long)]
|
|
||||||
remove: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
story: bool,
|
|
||||||
},
|
|
||||||
SendReceipt {
|
|
||||||
recipient: String,
|
|
||||||
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 't', long = "target-timestamp")]
|
|
||||||
target_timestamp: Vec<u64>,
|
|
||||||
|
|
||||||
#[arg(value_enum, long)]
|
|
||||||
r#type: ReceiptType,
|
|
||||||
},
|
|
||||||
SendSyncRequest,
|
|
||||||
SendTyping {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 's', long)]
|
|
||||||
stop: bool,
|
|
||||||
},
|
|
||||||
SendUnpinMessage {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'a', long = "target-author")]
|
|
||||||
target_author: String,
|
|
||||||
|
|
||||||
#[arg(short = 't', long = "target-timestamp")]
|
|
||||||
target_timestamp: u64,
|
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
|
||||||
note_to_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
notify_self: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
story: bool,
|
|
||||||
},
|
|
||||||
SendMessageRequestResponse {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
r#type: MessageRequestResponseType,
|
|
||||||
},
|
|
||||||
SetPin {
|
|
||||||
pin: String,
|
|
||||||
},
|
|
||||||
StartChangeNumber {
|
|
||||||
number: String,
|
|
||||||
#[arg(short = 'v', long)]
|
|
||||||
voice: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
captcha: Option<String>,
|
|
||||||
},
|
|
||||||
SubmitRateLimitChallenge {
|
|
||||||
challenge: String,
|
|
||||||
captcha: String,
|
|
||||||
},
|
|
||||||
Trust {
|
|
||||||
recipient: String,
|
|
||||||
|
|
||||||
#[arg(short = 'a', long = "trust-all-known-keys")]
|
|
||||||
trust_all_known_keys: bool,
|
|
||||||
|
|
||||||
#[arg(short = 'v', long = "verified-safety-number")]
|
|
||||||
verified_safety_number: Option<String>,
|
|
||||||
},
|
|
||||||
#[command(rename_all = "kebab-case")]
|
|
||||||
Unblock {
|
|
||||||
recipient: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'g', long)]
|
|
||||||
group_id: Vec<String>,
|
|
||||||
},
|
|
||||||
Unregister {
|
|
||||||
#[arg(long = "delete-account")]
|
|
||||||
delete_account: bool,
|
|
||||||
},
|
|
||||||
UpdateAccount {
|
|
||||||
#[arg(short = 'n', long = "device-name")]
|
|
||||||
device_name: Option<String>,
|
|
||||||
#[arg(long = "unrestricted-unidentified-sender")]
|
|
||||||
unrestricted_unidentified_sender: Option<bool>,
|
|
||||||
#[arg(long = "discoverable-by-number")]
|
|
||||||
discoverable_by_number: Option<bool>,
|
|
||||||
#[arg(long = "number-sharing")]
|
|
||||||
number_sharing: Option<bool>,
|
|
||||||
#[arg(short = 'u', long = "username")]
|
|
||||||
username: Option<String>,
|
|
||||||
#[arg(long = "delete-username")]
|
|
||||||
delete_username: bool,
|
|
||||||
},
|
|
||||||
UpdateConfiguration {
|
|
||||||
#[arg(long = "read-receipts")]
|
|
||||||
read_receipts: Option<bool>,
|
|
||||||
|
|
||||||
#[arg(long = "unidentified-delivery-indicators")]
|
|
||||||
unidentified_delivery_indicators: Option<bool>,
|
|
||||||
|
|
||||||
#[arg(long = "typing-indicators")]
|
|
||||||
typing_indicators: Option<bool>,
|
|
||||||
|
|
||||||
#[arg(long = "link-previews")]
|
|
||||||
link_previews: Option<bool>,
|
|
||||||
},
|
|
||||||
UpdateContact {
|
|
||||||
recipient: String,
|
|
||||||
|
|
||||||
#[arg(short = 'e', long)]
|
|
||||||
expiration: Option<u32>,
|
|
||||||
|
|
||||||
#[arg(short = 'n', long)]
|
|
||||||
name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "given-name")]
|
|
||||||
given_name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "family-name")]
|
|
||||||
family_name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "nick-given-name")]
|
|
||||||
nick_given_name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "nick-family-name")]
|
|
||||||
nick_family_name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
note: Option<String>,
|
|
||||||
},
|
|
||||||
UpdateDevice {
|
|
||||||
#[arg(short = 'd', long = "device-id")]
|
|
||||||
device_id: u32,
|
|
||||||
|
|
||||||
#[arg(short = 'n', long = "device-name")]
|
|
||||||
device_name: String,
|
|
||||||
},
|
|
||||||
UpdateGroup {
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
|
||||||
group_id: Option<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'n', long)]
|
|
||||||
name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'd', long)]
|
|
||||||
description: Option<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'a', long)]
|
|
||||||
avatar: Option<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'm', long)]
|
|
||||||
member: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(short = 'r', long = "remove-member")]
|
|
||||||
remove_member: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
admin: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long = "remove-admin")]
|
|
||||||
remove_admin: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
ban: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
unban: Vec<String>,
|
|
||||||
|
|
||||||
#[arg(long = "reset-link")]
|
|
||||||
reset_link: bool,
|
|
||||||
|
|
||||||
#[arg(value_enum, long)]
|
|
||||||
link: Option<LinkState>,
|
|
||||||
|
|
||||||
#[arg(value_enum, long = "set-permission-add-member")]
|
|
||||||
set_permission_add_member: Option<GroupPermission>,
|
|
||||||
|
|
||||||
#[arg(value_enum, long = "set-permission-edit-details")]
|
|
||||||
set_permission_edit_details: Option<GroupPermission>,
|
|
||||||
|
|
||||||
#[arg(value_enum, long = "set-permission-send-messages")]
|
|
||||||
set_permission_send_messages: Option<GroupPermission>,
|
|
||||||
|
|
||||||
#[arg(short = 'e', long)]
|
|
||||||
expiration: Option<u32>,
|
|
||||||
|
|
||||||
#[arg(long = "member-label-emoji")]
|
|
||||||
member_label_emoji: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "member-label")]
|
|
||||||
member_label: Option<String>,
|
|
||||||
},
|
|
||||||
UpdateProfile {
|
|
||||||
#[arg(long = "given-name")]
|
|
||||||
given_name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "family-name")]
|
|
||||||
family_name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
about: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "about-emoji")]
|
|
||||||
about_emoji: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")]
|
|
||||||
mobile_coin_address: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
avatar: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long = "remove-avatar")]
|
|
||||||
remove_avatar: bool,
|
|
||||||
},
|
|
||||||
UploadStickerPack {
|
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
Verify {
|
|
||||||
verification_code: String,
|
|
||||||
|
|
||||||
#[arg(short = 'p', long)]
|
|
||||||
pin: Option<String>,
|
|
||||||
},
|
|
||||||
Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
|
||||||
#[value(rename_all = "kebab-case")]
|
|
||||||
pub enum ReceiptType {
|
|
||||||
Read,
|
|
||||||
Viewed,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
|
||||||
#[value(rename_all = "kebab-case")]
|
|
||||||
pub enum LinkState {
|
|
||||||
Enabled,
|
|
||||||
EnabledWithApproval,
|
|
||||||
Disabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
|
||||||
#[value(rename_all = "kebab-case")]
|
|
||||||
pub enum GroupPermission {
|
|
||||||
EveryMember,
|
|
||||||
OnlyAdmins,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
|
||||||
#[value(rename_all = "kebab-case")]
|
|
||||||
pub enum MessageRequestResponseType {
|
|
||||||
Accept,
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
@ -1,532 +0,0 @@
|
|||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use jsonrpsee::async_client::ClientBuilder;
|
|
||||||
use jsonrpsee::core::client::{Error, SubscriptionClientT};
|
|
||||||
use jsonrpsee::http_client::HttpClientBuilder;
|
|
||||||
use jsonrpsee::proc_macros::rpc;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::Value;
|
|
||||||
use tokio::net::ToSocketAddrs;
|
|
||||||
|
|
||||||
#[rpc(client)]
|
|
||||||
pub trait Rpc {
|
|
||||||
#[method(name = "addDevice", param_kind = map)]
|
|
||||||
async fn add_device(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
uri: String,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "addStickerPack", param_kind = map)]
|
|
||||||
async fn add_sticker_pack(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
uri: String,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "block", param_kind = map)]
|
|
||||||
fn block(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "deleteLocalAccountData", param_kind = map)]
|
|
||||||
fn delete_local_account_data(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] ignoreRegistered: Option<bool>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "getAttachment", param_kind = map)]
|
|
||||||
fn get_attachment(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
id: String,
|
|
||||||
recipient: Option<String>,
|
|
||||||
#[allow(non_snake_case)] groupId: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "getAvatar", param_kind = map)]
|
|
||||||
fn get_avatar(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
contact: Option<String>,
|
|
||||||
profile: Option<String>,
|
|
||||||
#[allow(non_snake_case)] groupId: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "getSticker", param_kind = map)]
|
|
||||||
fn get_sticker(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] packId: String,
|
|
||||||
#[allow(non_snake_case)] stickerId: u32,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "getUserStatus", param_kind = map)]
|
|
||||||
fn get_user_status(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "joinGroup", param_kind = map)]
|
|
||||||
fn join_group(&self, account: Option<String>, uri: String) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[method(name = "finishChangeNumber", param_kind = map)]
|
|
||||||
fn finish_change_number(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
number: String,
|
|
||||||
verificationCode: String,
|
|
||||||
pin: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "finishLink", param_kind = map)]
|
|
||||||
fn finish_link(
|
|
||||||
&self,
|
|
||||||
#[allow(non_snake_case)] deviceLinkUri: String,
|
|
||||||
#[allow(non_snake_case)] deviceName: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "listAccounts", param_kind = map)]
|
|
||||||
fn list_accounts(&self) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "listContacts", param_kind = map)]
|
|
||||||
fn list_contacts(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] allRecipients: bool,
|
|
||||||
blocked: Option<bool>,
|
|
||||||
name: Option<String>,
|
|
||||||
detailed: bool,
|
|
||||||
internal: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "listDevices", param_kind = map)]
|
|
||||||
fn list_devices(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "listGroups", param_kind = map)]
|
|
||||||
fn list_groups(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "listIdentities", param_kind = map)]
|
|
||||||
fn list_identities(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
number: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "listStickerPacks", param_kind = map)]
|
|
||||||
fn list_sticker_packs(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "quitGroup", param_kind = map)]
|
|
||||||
fn quit_group(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] groupId: String,
|
|
||||||
delete: bool,
|
|
||||||
admins: Vec<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "register", param_kind = map)]
|
|
||||||
fn register(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
voice: bool,
|
|
||||||
captcha: Option<String>,
|
|
||||||
reregister: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "removeContact", param_kind = map)]
|
|
||||||
fn remove_contact(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipient: String,
|
|
||||||
forget: bool,
|
|
||||||
hide: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "removeDevice", param_kind = map)]
|
|
||||||
fn remove_device(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] deviceId: u32,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "removePin", param_kind = map)]
|
|
||||||
fn remove_pin(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "remoteDelete", param_kind = map)]
|
|
||||||
fn remote_delete(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] targetTimestamp: u64,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[method(name = "send", param_kind = map)]
|
|
||||||
fn send(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
groupIds: Vec<String>,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] notifySelf: bool,
|
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
|
||||||
#[allow(non_snake_case)] endSession: bool,
|
|
||||||
message: String,
|
|
||||||
attachments: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] viewOnce: bool,
|
|
||||||
mentions: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] textStyle: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] quoteTimestamp: Option<u64>,
|
|
||||||
#[allow(non_snake_case)] quoteAuthor: Option<String>,
|
|
||||||
#[allow(non_snake_case)] quoteMessage: Option<String>,
|
|
||||||
#[allow(non_snake_case)] quoteMention: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] quoteTextStyle: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] quoteAttachment: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] previewUrl: Option<String>,
|
|
||||||
#[allow(non_snake_case)] previewTitle: Option<String>,
|
|
||||||
#[allow(non_snake_case)] previewDescription: Option<String>,
|
|
||||||
#[allow(non_snake_case)] previewImage: Option<String>,
|
|
||||||
sticker: Option<String>,
|
|
||||||
#[allow(non_snake_case)] storyTimestamp: Option<u64>,
|
|
||||||
#[allow(non_snake_case)] storyAuthor: Option<String>,
|
|
||||||
#[allow(non_snake_case)] editTimestamp: Option<u64>,
|
|
||||||
#[allow(non_snake_case)] noUrgent: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendContacts", param_kind = map)]
|
|
||||||
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendAdminDelete", param_kind = map)]
|
|
||||||
fn send_admin_delete(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] targetAuthor: String,
|
|
||||||
#[allow(non_snake_case)] targetTimestamp: u64,
|
|
||||||
story: bool,
|
|
||||||
#[allow(non_snake_case)] notifySelf: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendPinMessage", param_kind = map)]
|
|
||||||
fn send_pin_message(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] targetAuthor: String,
|
|
||||||
#[allow(non_snake_case)] targetTimestamp: u64,
|
|
||||||
#[allow(non_snake_case)] pinDuration: Option<i32>,
|
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
|
||||||
#[allow(non_snake_case)] notifySelf: bool,
|
|
||||||
story: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendPollCreate", param_kind = map)]
|
|
||||||
fn send_poll_create(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
question: String,
|
|
||||||
option: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] noMulti: bool,
|
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
|
||||||
#[allow(non_snake_case)] notifySelf: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendPollVote", param_kind = map)]
|
|
||||||
fn send_poll_vote(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] pollAuthor: Option<String>,
|
|
||||||
#[allow(non_snake_case)] pollTimestamp: u64,
|
|
||||||
option: Vec<i32>,
|
|
||||||
#[allow(non_snake_case)] voteCount: i32,
|
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
|
||||||
#[allow(non_snake_case)] notifySelf: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendPollTerminate", param_kind = map)]
|
|
||||||
fn send_poll_terminate(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] pollTimestamp: u64,
|
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
|
||||||
#[allow(non_snake_case)] notifySelf: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendUnpinMessage", param_kind = map)]
|
|
||||||
fn send_unpin_message(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] targetAuthor: String,
|
|
||||||
#[allow(non_snake_case)] targetTimestamp: u64,
|
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
|
||||||
#[allow(non_snake_case)] notifySelf: bool,
|
|
||||||
story: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendPaymentNotification", param_kind = map)]
|
|
||||||
fn send_payment_notification(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipient: String,
|
|
||||||
receipt: String,
|
|
||||||
note: String,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendReaction", param_kind = map)]
|
|
||||||
fn send_reaction(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
|
||||||
#[allow(non_snake_case)] notifySelf: bool,
|
|
||||||
emoji: String,
|
|
||||||
#[allow(non_snake_case)] targetAuthor: String,
|
|
||||||
#[allow(non_snake_case)] targetTimestamp: u64,
|
|
||||||
remove: bool,
|
|
||||||
story: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendReceipt", param_kind = map)]
|
|
||||||
fn send_receipt(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipient: String,
|
|
||||||
usernames: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] targetTimestamps: Vec<u64>,
|
|
||||||
r#type: String,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendSyncRequest", param_kind = map)]
|
|
||||||
fn send_sync_request(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendTyping", param_kind = map)]
|
|
||||||
fn send_typing(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
stop: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "sendMessageRequestResponse", param_kind = map)]
|
|
||||||
fn send_message_request_response(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
r#type: String,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "setPin", param_kind = map)]
|
|
||||||
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "submitRateLimitChallenge", param_kind = map)]
|
|
||||||
fn submit_rate_limit_challenge(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
challenge: String,
|
|
||||||
captcha: String,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "startChangeNumber", param_kind = map)]
|
|
||||||
fn start_change_number(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
number: String,
|
|
||||||
voice: bool,
|
|
||||||
captcha: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "startLink", param_kind = map)]
|
|
||||||
fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "trust", param_kind = map)]
|
|
||||||
fn trust(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipient: String,
|
|
||||||
#[allow(non_snake_case)] trustAllKnownKeys: bool,
|
|
||||||
#[allow(non_snake_case)] verifiedSafetyNumber: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "unblock", param_kind = map)]
|
|
||||||
fn unblock(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipients: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "unregister", param_kind = map)]
|
|
||||||
fn unregister(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] deleteAccount: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[method(name = "updateAccount", param_kind = map)]
|
|
||||||
fn update_account(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
deviceName: Option<String>,
|
|
||||||
unrestrictedUnidentifiedSender: Option<bool>,
|
|
||||||
discoverableByNumber: Option<bool>,
|
|
||||||
numberSharing: Option<bool>,
|
|
||||||
username: Option<String>,
|
|
||||||
deleteUsername: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "updateConfiguration", param_kind = map)]
|
|
||||||
fn update_configuration(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] readReceipts: Option<bool>,
|
|
||||||
#[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>,
|
|
||||||
#[allow(non_snake_case)] typingIndicators: Option<bool>,
|
|
||||||
#[allow(non_snake_case)] linkPreviews: Option<bool>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "updateContact", param_kind = map)]
|
|
||||||
fn update_contact(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
recipient: String,
|
|
||||||
name: Option<String>,
|
|
||||||
expiration: Option<u32>,
|
|
||||||
#[allow(non_snake_case)] givenName: Option<String>,
|
|
||||||
#[allow(non_snake_case)] familyName: Option<String>,
|
|
||||||
#[allow(non_snake_case)] nickGivenName: Option<String>,
|
|
||||||
#[allow(non_snake_case)] nickFamilyName: Option<String>,
|
|
||||||
note: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "updateDevice", param_kind = map)]
|
|
||||||
fn update_device(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] deviceId: u32,
|
|
||||||
#[allow(non_snake_case)] deviceName: String,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "updateGroup", param_kind = map)]
|
|
||||||
fn update_group(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] groupId: Option<String>,
|
|
||||||
name: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
avatar: Option<String>,
|
|
||||||
member: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] removeMember: Vec<String>,
|
|
||||||
admin: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] removeAdmin: Vec<String>,
|
|
||||||
ban: Vec<String>,
|
|
||||||
unban: Vec<String>,
|
|
||||||
#[allow(non_snake_case)] resetLink: bool,
|
|
||||||
#[allow(non_snake_case)] link: Option<String>,
|
|
||||||
#[allow(non_snake_case)] setPermissionAddMember: Option<String>,
|
|
||||||
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
|
|
||||||
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
|
|
||||||
expiration: Option<u32>,
|
|
||||||
#[allow(non_snake_case)] memberLabelEmoji: Option<String>,
|
|
||||||
#[allow(non_snake_case)] memberLabel: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "updateProfile", param_kind = map)]
|
|
||||||
fn update_profile(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] givenName: Option<String>,
|
|
||||||
#[allow(non_snake_case)] familyName: Option<String>,
|
|
||||||
about: Option<String>,
|
|
||||||
#[allow(non_snake_case)] aboutEmoji: Option<String>,
|
|
||||||
#[allow(non_snake_case)] mobileCoinAddress: Option<String>,
|
|
||||||
avatar: Option<String>,
|
|
||||||
#[allow(non_snake_case)] removeAvatar: bool,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "uploadStickerPack", param_kind = map)]
|
|
||||||
fn upload_sticker_pack(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
path: String,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[method(name = "verify", param_kind = map)]
|
|
||||||
fn verify(
|
|
||||||
&self,
|
|
||||||
account: Option<String>,
|
|
||||||
#[allow(non_snake_case)] verificationCode: String,
|
|
||||||
pin: Option<String>,
|
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
|
|
||||||
#[subscription(
|
|
||||||
name = "subscribeReceive" => "receive",
|
|
||||||
unsubscribe = "unsubscribeReceive",
|
|
||||||
item = Value,
|
|
||||||
param_kind = map
|
|
||||||
)]
|
|
||||||
async fn subscribe_receive(&self, account: Option<String>) -> SubscriptionResult;
|
|
||||||
|
|
||||||
#[method(name = "version")]
|
|
||||||
fn version(&self) -> Result<Value, ErrorObjectOwned>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct JsonLink {
|
|
||||||
pub device_link_uri: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_tcp(
|
|
||||||
tcp: impl ToSocketAddrs,
|
|
||||||
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
|
||||||
let (sender, receiver) = super::transports::tcp::connect(tcp).await?;
|
|
||||||
|
|
||||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub async fn connect_unix(
|
|
||||||
socket_path: impl AsRef<Path>,
|
|
||||||
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
|
||||||
let (sender, receiver) = super::transports::ipc::connect(socket_path).await?;
|
|
||||||
|
|
||||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> {
|
|
||||||
HttpClientBuilder::default().build(uri)
|
|
||||||
}
|
|
||||||
@ -1,728 +0,0 @@
|
|||||||
use std::{path::PathBuf, time::Duration};
|
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
use jsonrpsee::core::client::{Error as RpcError, Subscription, SubscriptionClientT};
|
|
||||||
use serde_json::{Error, Value};
|
|
||||||
use tokio::{select, time::sleep};
|
|
||||||
|
|
||||||
use cli::Cli;
|
|
||||||
|
|
||||||
use crate::cli::{CliCommands, GroupPermission, LinkState};
|
|
||||||
use crate::jsonrpc::RpcClient;
|
|
||||||
|
|
||||||
mod cli;
|
|
||||||
#[allow(non_snake_case, clippy::too_many_arguments)]
|
|
||||||
mod jsonrpc;
|
|
||||||
mod transports;
|
|
||||||
|
|
||||||
const DEFAULT_TCP: &str = "127.0.0.1:7583";
|
|
||||||
const DEFAULT_SOCKET_SUFFIX: &str = "signal-cli/socket";
|
|
||||||
const DEFAULT_HTTP: &str = "http://localhost:8080/api/v1/rpc";
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), anyhow::Error> {
|
|
||||||
let cli = cli::Cli::parse();
|
|
||||||
|
|
||||||
let result = connect(cli).await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(Value::Null) => {}
|
|
||||||
Ok(v) => println!("{v}"),
|
|
||||||
Err(e) => return Err(anyhow::anyhow!("JSON-RPC command failed: {e:?}")),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_command(
|
|
||||||
cli: Cli,
|
|
||||||
client: impl SubscriptionClientT + Sync,
|
|
||||||
) -> Result<Value, RpcError> {
|
|
||||||
match cli.command {
|
|
||||||
CliCommands::Receive { timeout } => {
|
|
||||||
let mut stream = client.subscribe_receive(cli.account).await?;
|
|
||||||
|
|
||||||
{
|
|
||||||
while let Some(v) = stream_next(timeout, &mut stream).await {
|
|
||||||
let v = v?;
|
|
||||||
println!("{v}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stream.unsubscribe().await?;
|
|
||||||
Ok(Value::Null)
|
|
||||||
}
|
|
||||||
CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
|
|
||||||
CliCommands::Block {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
} => client.block(cli.account, recipient, group_id).await,
|
|
||||||
CliCommands::DeleteLocalAccountData { ignore_registered } => {
|
|
||||||
client
|
|
||||||
.delete_local_account_data(cli.account, ignore_registered)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::GetUserStatus {
|
|
||||||
recipient,
|
|
||||||
username,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.get_user_status(cli.account, recipient, username)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
|
||||||
CliCommands::Link { name } => {
|
|
||||||
let url = client
|
|
||||||
.start_link(cli.account)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
|
||||||
.device_link_uri;
|
|
||||||
println!("{url}");
|
|
||||||
client.finish_link(url, name).await
|
|
||||||
}
|
|
||||||
CliCommands::ListAccounts => client.list_accounts().await,
|
|
||||||
CliCommands::ListContacts {
|
|
||||||
recipient,
|
|
||||||
all_recipients,
|
|
||||||
blocked,
|
|
||||||
name,
|
|
||||||
detailed,
|
|
||||||
internal,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.list_contacts(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
all_recipients,
|
|
||||||
blocked,
|
|
||||||
name,
|
|
||||||
detailed,
|
|
||||||
internal,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::ListDevices => client.list_devices(cli.account).await,
|
|
||||||
CliCommands::ListGroups {
|
|
||||||
detailed: _,
|
|
||||||
group_id,
|
|
||||||
} => client.list_groups(cli.account, group_id).await,
|
|
||||||
CliCommands::ListIdentities { number } => client.list_identities(cli.account, number).await,
|
|
||||||
CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
|
|
||||||
CliCommands::QuitGroup {
|
|
||||||
group_id,
|
|
||||||
delete,
|
|
||||||
admin,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.quit_group(cli.account, group_id, delete, admin)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::Register {
|
|
||||||
voice,
|
|
||||||
captcha,
|
|
||||||
reregister,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.register(cli.account, voice, captcha, reregister)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::RemoveContact {
|
|
||||||
recipient,
|
|
||||||
forget,
|
|
||||||
hide,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.remove_contact(cli.account, recipient, forget, hide)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::RemoveDevice { device_id } => {
|
|
||||||
client.remove_device(cli.account, device_id).await
|
|
||||||
}
|
|
||||||
CliCommands::RemovePin => client.remove_pin(cli.account).await,
|
|
||||||
CliCommands::RemoteDelete {
|
|
||||||
target_timestamp,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
note_to_self,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.remote_delete(
|
|
||||||
cli.account,
|
|
||||||
target_timestamp,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
note_to_self,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::Send {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
notify_self,
|
|
||||||
note_to_self,
|
|
||||||
end_session,
|
|
||||||
message,
|
|
||||||
message_from_stdin,
|
|
||||||
attachment,
|
|
||||||
view_once,
|
|
||||||
mention,
|
|
||||||
text_style,
|
|
||||||
quote_timestamp,
|
|
||||||
quote_author,
|
|
||||||
quote_message,
|
|
||||||
quote_mention,
|
|
||||||
quote_text_style,
|
|
||||||
quote_attachment,
|
|
||||||
preview_url,
|
|
||||||
preview_title,
|
|
||||||
preview_description,
|
|
||||||
preview_image,
|
|
||||||
sticker,
|
|
||||||
story_timestamp,
|
|
||||||
story_author,
|
|
||||||
edit_timestamp,
|
|
||||||
no_urgent,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
notify_self,
|
|
||||||
note_to_self,
|
|
||||||
end_session,
|
|
||||||
if message_from_stdin {
|
|
||||||
std::io::read_to_string(std::io::stdin()).unwrap()
|
|
||||||
} else {
|
|
||||||
message.unwrap_or_default()
|
|
||||||
},
|
|
||||||
attachment,
|
|
||||||
view_once,
|
|
||||||
mention,
|
|
||||||
text_style,
|
|
||||||
quote_timestamp,
|
|
||||||
quote_author,
|
|
||||||
quote_message,
|
|
||||||
quote_mention,
|
|
||||||
quote_text_style,
|
|
||||||
quote_attachment,
|
|
||||||
preview_url,
|
|
||||||
preview_title,
|
|
||||||
preview_description,
|
|
||||||
preview_image,
|
|
||||||
sticker,
|
|
||||||
story_timestamp,
|
|
||||||
story_author,
|
|
||||||
edit_timestamp,
|
|
||||||
no_urgent,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendContacts => client.send_contacts(cli.account).await,
|
|
||||||
CliCommands::SendAdminDelete {
|
|
||||||
group_id,
|
|
||||||
target_author,
|
|
||||||
target_timestamp,
|
|
||||||
story,
|
|
||||||
notify_self,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_admin_delete(
|
|
||||||
cli.account,
|
|
||||||
group_id,
|
|
||||||
target_author,
|
|
||||||
target_timestamp,
|
|
||||||
story,
|
|
||||||
notify_self,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendPaymentNotification {
|
|
||||||
recipient,
|
|
||||||
receipt,
|
|
||||||
note,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_payment_notification(cli.account, recipient, receipt, note)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendPinMessage {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
target_author,
|
|
||||||
target_timestamp,
|
|
||||||
pin_duration,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
story,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_pin_message(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
target_author,
|
|
||||||
target_timestamp,
|
|
||||||
pin_duration,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
story,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendPollCreate {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
question,
|
|
||||||
option,
|
|
||||||
no_multi,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_poll_create(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
question,
|
|
||||||
option,
|
|
||||||
no_multi,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendPollTerminate {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
poll_timestamp,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_poll_terminate(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
poll_timestamp,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendPollVote {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
poll_author,
|
|
||||||
poll_timestamp,
|
|
||||||
option,
|
|
||||||
vote_count,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_poll_vote(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
poll_author,
|
|
||||||
poll_timestamp,
|
|
||||||
option,
|
|
||||||
vote_count,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendReaction {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
emoji,
|
|
||||||
target_author,
|
|
||||||
target_timestamp,
|
|
||||||
remove,
|
|
||||||
story,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_reaction(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
emoji,
|
|
||||||
target_author,
|
|
||||||
target_timestamp,
|
|
||||||
remove,
|
|
||||||
story,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendReceipt {
|
|
||||||
recipient,
|
|
||||||
username,
|
|
||||||
target_timestamp,
|
|
||||||
r#type,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_receipt(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
username,
|
|
||||||
target_timestamp,
|
|
||||||
match r#type {
|
|
||||||
cli::ReceiptType::Read => "read".to_owned(),
|
|
||||||
cli::ReceiptType::Viewed => "viewed".to_owned(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
|
|
||||||
CliCommands::SendTyping {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
stop,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_typing(cli.account, recipient, group_id, stop)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendUnpinMessage {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
target_author,
|
|
||||||
target_timestamp,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
story,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_unpin_message(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
username,
|
|
||||||
target_author,
|
|
||||||
target_timestamp,
|
|
||||||
note_to_self,
|
|
||||||
notify_self,
|
|
||||||
story,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
|
|
||||||
CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
|
|
||||||
client
|
|
||||||
.submit_rate_limit_challenge(cli.account, challenge, captcha)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::Trust {
|
|
||||||
recipient,
|
|
||||||
trust_all_known_keys,
|
|
||||||
verified_safety_number,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.trust(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
trust_all_known_keys,
|
|
||||||
verified_safety_number,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::Unblock {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
} => client.unblock(cli.account, recipient, group_id).await,
|
|
||||||
CliCommands::Unregister { delete_account } => {
|
|
||||||
client.unregister(cli.account, delete_account).await
|
|
||||||
}
|
|
||||||
CliCommands::UpdateAccount {
|
|
||||||
device_name,
|
|
||||||
unrestricted_unidentified_sender,
|
|
||||||
discoverable_by_number,
|
|
||||||
number_sharing,
|
|
||||||
username,
|
|
||||||
delete_username,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.update_account(
|
|
||||||
cli.account,
|
|
||||||
device_name,
|
|
||||||
unrestricted_unidentified_sender,
|
|
||||||
discoverable_by_number,
|
|
||||||
number_sharing,
|
|
||||||
username,
|
|
||||||
delete_username,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::UpdateConfiguration {
|
|
||||||
read_receipts,
|
|
||||||
unidentified_delivery_indicators,
|
|
||||||
typing_indicators,
|
|
||||||
link_previews,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.update_configuration(
|
|
||||||
cli.account,
|
|
||||||
read_receipts,
|
|
||||||
unidentified_delivery_indicators,
|
|
||||||
typing_indicators,
|
|
||||||
link_previews,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::UpdateContact {
|
|
||||||
recipient,
|
|
||||||
expiration,
|
|
||||||
name,
|
|
||||||
given_name,
|
|
||||||
family_name,
|
|
||||||
nick_given_name,
|
|
||||||
nick_family_name,
|
|
||||||
note,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.update_contact(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
name,
|
|
||||||
expiration,
|
|
||||||
given_name,
|
|
||||||
family_name,
|
|
||||||
nick_given_name,
|
|
||||||
nick_family_name,
|
|
||||||
note,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::UpdateDevice {
|
|
||||||
device_id,
|
|
||||||
device_name,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.update_device(cli.account, device_id, device_name)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::UpdateGroup {
|
|
||||||
group_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
avatar,
|
|
||||||
member,
|
|
||||||
remove_member,
|
|
||||||
admin,
|
|
||||||
remove_admin,
|
|
||||||
ban,
|
|
||||||
unban,
|
|
||||||
reset_link,
|
|
||||||
link,
|
|
||||||
set_permission_add_member,
|
|
||||||
set_permission_edit_details,
|
|
||||||
set_permission_send_messages,
|
|
||||||
expiration,
|
|
||||||
member_label_emoji,
|
|
||||||
member_label,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.update_group(
|
|
||||||
cli.account,
|
|
||||||
group_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
avatar,
|
|
||||||
member,
|
|
||||||
remove_member,
|
|
||||||
admin,
|
|
||||||
remove_admin,
|
|
||||||
ban,
|
|
||||||
unban,
|
|
||||||
reset_link,
|
|
||||||
link.map(|link| match link {
|
|
||||||
LinkState::Enabled => "enabled".to_owned(),
|
|
||||||
LinkState::EnabledWithApproval => "enabledWithApproval".to_owned(),
|
|
||||||
LinkState::Disabled => "disabled".to_owned(),
|
|
||||||
}),
|
|
||||||
set_permission_add_member.map(|p| match p {
|
|
||||||
GroupPermission::EveryMember => "everyMember".to_owned(),
|
|
||||||
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
|
||||||
}),
|
|
||||||
set_permission_edit_details.map(|p| match p {
|
|
||||||
GroupPermission::EveryMember => "everyMember".to_owned(),
|
|
||||||
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
|
||||||
}),
|
|
||||||
set_permission_send_messages.map(|p| match p {
|
|
||||||
GroupPermission::EveryMember => "everyMember".to_owned(),
|
|
||||||
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
|
||||||
}),
|
|
||||||
expiration,
|
|
||||||
member_label_emoji,
|
|
||||||
member_label,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::UpdateProfile {
|
|
||||||
given_name,
|
|
||||||
family_name,
|
|
||||||
about,
|
|
||||||
about_emoji,
|
|
||||||
mobile_coin_address,
|
|
||||||
avatar,
|
|
||||||
remove_avatar,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.update_profile(
|
|
||||||
cli.account,
|
|
||||||
given_name,
|
|
||||||
family_name,
|
|
||||||
about,
|
|
||||||
about_emoji,
|
|
||||||
mobile_coin_address,
|
|
||||||
avatar,
|
|
||||||
remove_avatar,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::UploadStickerPack { path } => {
|
|
||||||
client.upload_sticker_pack(cli.account, path).await
|
|
||||||
}
|
|
||||||
CliCommands::Verify {
|
|
||||||
verification_code,
|
|
||||||
pin,
|
|
||||||
} => client.verify(cli.account, verification_code, pin).await,
|
|
||||||
CliCommands::Version => client.version().await,
|
|
||||||
CliCommands::AddStickerPack { uri } => client.add_sticker_pack(cli.account, uri).await,
|
|
||||||
CliCommands::FinishChangeNumber {
|
|
||||||
number,
|
|
||||||
verification_code,
|
|
||||||
pin,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.finish_change_number(cli.account, number, verification_code, pin)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::GetAttachment {
|
|
||||||
id,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.get_attachment(cli.account, id, recipient, group_id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::GetAvatar {
|
|
||||||
contact,
|
|
||||||
profile,
|
|
||||||
group_id,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.get_avatar(cli.account, contact, profile, group_id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::GetSticker {
|
|
||||||
pack_id,
|
|
||||||
sticker_id,
|
|
||||||
} => client.get_sticker(cli.account, pack_id, sticker_id).await,
|
|
||||||
CliCommands::StartChangeNumber {
|
|
||||||
number,
|
|
||||||
voice,
|
|
||||||
captcha,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.start_change_number(cli.account, number, voice, captcha)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
CliCommands::SendMessageRequestResponse {
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
r#type,
|
|
||||||
} => {
|
|
||||||
client
|
|
||||||
.send_message_request_response(
|
|
||||||
cli.account,
|
|
||||||
recipient,
|
|
||||||
group_id,
|
|
||||||
match r#type {
|
|
||||||
cli::MessageRequestResponseType::Accept => "accept".to_owned(),
|
|
||||||
cli::MessageRequestResponseType::Delete => "delete".to_owned(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect(cli: Cli) -> Result<Value, RpcError> {
|
|
||||||
if let Some(http) = &cli.json_rpc_http {
|
|
||||||
let uri = if let Some(uri) = http {
|
|
||||||
uri
|
|
||||||
} else {
|
|
||||||
DEFAULT_HTTP
|
|
||||||
};
|
|
||||||
let client = jsonrpc::connect_http(uri)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
|
||||||
|
|
||||||
handle_command(cli, client).await
|
|
||||||
} else if let Some(tcp) = cli.json_rpc_tcp {
|
|
||||||
let socket_addr = tcp.unwrap_or_else(|| DEFAULT_TCP.parse().unwrap());
|
|
||||||
let client = jsonrpc::connect_tcp(socket_addr)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
|
||||||
|
|
||||||
handle_command(cli, client).await
|
|
||||||
} else {
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
Err(RpcError::Custom("Invalid socket".into()))
|
|
||||||
}
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
let socket_path = cli
|
|
||||||
.json_rpc_socket
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(None)
|
|
||||||
.or_else(|| {
|
|
||||||
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
|
||||||
PathBuf::from(runtime_dir)
|
|
||||||
.join(DEFAULT_SOCKET_SUFFIX)
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
|
||||||
let client = jsonrpc::connect_unix(socket_path)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
|
||||||
|
|
||||||
handle_command(cli, client).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stream_next(
|
|
||||||
timeout: f64,
|
|
||||||
stream: &mut Subscription<Value>,
|
|
||||||
) -> Option<Result<Value, Error>> {
|
|
||||||
if timeout < 0.0 {
|
|
||||||
stream.next().await
|
|
||||||
} else {
|
|
||||||
select! {
|
|
||||||
v = stream.next() => v,
|
|
||||||
_= sleep(Duration::from_millis((timeout * 1000.0) as u64)) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
use std::io::Error;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use futures_util::stream::StreamExt;
|
|
||||||
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
|
||||||
use tokio::net::UnixStream;
|
|
||||||
use tokio_util::codec::Decoder;
|
|
||||||
|
|
||||||
use super::stream_codec::StreamCodec;
|
|
||||||
use super::{Receiver, Sender};
|
|
||||||
|
|
||||||
/// Connect to a JSON-RPC Unix Socket server.
|
|
||||||
pub async fn connect(
|
|
||||||
socket: impl AsRef<Path>,
|
|
||||||
) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
|
|
||||||
let connection = UnixStream::connect(socket).await?;
|
|
||||||
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
|
|
||||||
|
|
||||||
let sender = Sender { inner: sink };
|
|
||||||
let receiver = Receiver { inner: stream };
|
|
||||||
|
|
||||||
Ok((sender, receiver))
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
|
||||||
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub mod ipc;
|
|
||||||
mod stream_codec;
|
|
||||||
pub mod tcp;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
enum Errors {
|
|
||||||
#[error("Other: {0}")]
|
|
||||||
Other(String),
|
|
||||||
#[error("Closed")]
|
|
||||||
Closed,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Sender<T: Send + Sink<String>> {
|
|
||||||
inner: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
|
||||||
for Sender<T>
|
|
||||||
{
|
|
||||||
type Error = Errors;
|
|
||||||
|
|
||||||
async fn send(&mut self, body: String) -> Result<(), Self::Error> {
|
|
||||||
self.inner
|
|
||||||
.send(body)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn close(&mut self) -> Result<(), Self::Error> {
|
|
||||||
self.inner
|
|
||||||
.close()
|
|
||||||
.await
|
|
||||||
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Receiver<T: Send + Stream> {
|
|
||||||
inner: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
|
||||||
for Receiver<T>
|
|
||||||
{
|
|
||||||
type Error = Errors;
|
|
||||||
|
|
||||||
async fn receive(&mut self) -> Result<ReceivedMessage, Self::Error> {
|
|
||||||
match self.inner.next().await {
|
|
||||||
None => Err(Errors::Closed),
|
|
||||||
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
|
||||||
Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
use bytes::BytesMut;
|
|
||||||
use std::{io, str};
|
|
||||||
use tokio_util::codec::{Decoder, Encoder};
|
|
||||||
|
|
||||||
type Separator = u8;
|
|
||||||
|
|
||||||
/// Stream codec for streaming protocols (ipc, tcp)
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct StreamCodec {
|
|
||||||
incoming_separator: Separator,
|
|
||||||
outgoing_separator: Separator,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StreamCodec {
|
|
||||||
/// Default codec with streaming input data. Input can be both enveloped and not.
|
|
||||||
pub fn stream_incoming() -> Self {
|
|
||||||
StreamCodec::new(b'\n', b'\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
/// New custom stream codec
|
|
||||||
pub fn new(incoming_separator: Separator, outgoing_separator: Separator) -> Self {
|
|
||||||
StreamCodec {
|
|
||||||
incoming_separator,
|
|
||||||
outgoing_separator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Decoder for StreamCodec {
|
|
||||||
type Item = String;
|
|
||||||
type Error = io::Error;
|
|
||||||
|
|
||||||
fn decode(&mut self, buf: &mut BytesMut) -> io::Result<Option<Self::Item>> {
|
|
||||||
if let Some(i) = buf
|
|
||||||
.as_ref()
|
|
||||||
.iter()
|
|
||||||
.position(|&b| b == self.incoming_separator)
|
|
||||||
{
|
|
||||||
let line = buf.split_to(i);
|
|
||||||
let _ = buf.split_to(1);
|
|
||||||
|
|
||||||
match str::from_utf8(line.as_ref()) {
|
|
||||||
Ok(s) => Ok(Some(s.to_string())),
|
|
||||||
Err(_) => Err(io::Error::other("invalid UTF-8")),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Encoder<String> for StreamCodec {
|
|
||||||
type Error = io::Error;
|
|
||||||
|
|
||||||
fn encode(&mut self, msg: String, buf: &mut BytesMut) -> io::Result<()> {
|
|
||||||
let mut payload = msg.into_bytes();
|
|
||||||
payload.push(self.outgoing_separator);
|
|
||||||
buf.extend_from_slice(&payload);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
use std::io::Error;
|
|
||||||
|
|
||||||
use futures_util::stream::StreamExt;
|
|
||||||
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
|
||||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
|
||||||
use tokio_util::codec::Decoder;
|
|
||||||
|
|
||||||
use super::stream_codec::StreamCodec;
|
|
||||||
use super::{Receiver, Sender};
|
|
||||||
|
|
||||||
/// Connect to a JSON-RPC TCP server.
|
|
||||||
pub async fn connect(
|
|
||||||
socket: impl ToSocketAddrs,
|
|
||||||
) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
|
|
||||||
let connection = TcpStream::connect(socket).await?;
|
|
||||||
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
|
|
||||||
|
|
||||||
let sender = Sender { inner: sink };
|
|
||||||
let receiver = Receiver { inner: stream };
|
|
||||||
|
|
||||||
Ok((sender, receiver))
|
|
||||||
}
|
|
||||||
@ -1,148 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<component type="console-application">
|
|
||||||
<id>org.asamk.SignalCli</id>
|
|
||||||
|
|
||||||
<name>signal-cli</name>
|
|
||||||
<summary>Use Signal messenger in terminal</summary>
|
|
||||||
<developer id="org.asamk">
|
|
||||||
<name>AsamK</name>
|
|
||||||
</developer>
|
|
||||||
<icon type="stock">org.asamk.SignalCli</icon>
|
|
||||||
<keywords>
|
|
||||||
<keyword>signal</keyword>
|
|
||||||
<keyword>signal-cli</keyword>
|
|
||||||
<keyword>messenger</keyword>
|
|
||||||
<keyword>messaging</keyword>
|
|
||||||
</keywords>
|
|
||||||
|
|
||||||
<url type="bugtracker">https://github.com/AsamK/signal-cli/issues</url>
|
|
||||||
<url type="homepage">https://github.com/AsamK/signal-cli</url>
|
|
||||||
<url type="donation">https://github.com/sponsors/AsamK</url>
|
|
||||||
<url type="faq">https://github.com/AsamK/signal-cli/discussions</url>
|
|
||||||
<url type="vcs-browser">https://github.com/AsamK/signal-cli</url>
|
|
||||||
|
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
|
||||||
<project_license>GPL-3.0-only</project_license>
|
|
||||||
|
|
||||||
<description>
|
|
||||||
<p>
|
|
||||||
signal-cli is an unofficial commandline interface for the Signal Messenger.
|
|
||||||
It supports many Signal functions, including registering, verifying, sending and receiving messages.
|
|
||||||
For registering you need a phone number where you can receive SMS or incoming calls.
|
|
||||||
Alternatively signal-cli can be linked to an existing App account.
|
|
||||||
</p>
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<categories>
|
|
||||||
<category>Utility</category>
|
|
||||||
<category>Java</category>
|
|
||||||
</categories>
|
|
||||||
|
|
||||||
<provides>
|
|
||||||
<binary>signal-cli</binary>
|
|
||||||
</provides>
|
|
||||||
<content_rating type="oars-1.1">
|
|
||||||
<content_attribute id="social-chat">intense</content_attribute>
|
|
||||||
</content_rating>
|
|
||||||
<releases>
|
|
||||||
<release version="0.14.5" date="2026-06-11">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.5</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.14.4" date="2026-05-23">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.14.3" date="2026-04-22">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.3</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.14.2" date="2026-04-04">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.14.1" date="2026-03-08">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.1</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.14.0" date="2026-03-01">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.0</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.24" date="2026-02-05">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.24</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.23" date="2026-01-24">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.23</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.22" date="2025-11-14">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.22</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.21" date="2025-10-25">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.21</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.20" date="2025-09-23">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.20</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.19" date="2025-09-15">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.19</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.18" date="2025-07-16">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.18</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.17" date="2025-06-28">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.17</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.16" date="2025-06-07">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.16</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.15" date="2025-05-08">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.15</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.14" date="2025-04-06">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.14</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.13" date="2025-02-28">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.13</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.12" date="2025-01-18">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.12</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.11" date="2024-12-26">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.11</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.10" date="2024-11-30">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.10</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.9" date="2024-10-28">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.9</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.8" date="2024-10-26">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.8</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.7" date="2024-09-28">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.7</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.6" date="2024-09-08">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.6</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.5" date="2024-07-25">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.5</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.4" date="2024-06-06">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.4</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.3" date="2024-04-19">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.3</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.2" date="2024-03-23">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.2</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.1" date="2024-02-27">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.1</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.13.0" date="2024-02-18">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.0</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.12.8" date="2024-02-08">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.8</url>
|
|
||||||
</release>
|
|
||||||
<release version="0.12.7" date="2023-12-15">
|
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.7</url>
|
|
||||||
</release>
|
|
||||||
</releases>
|
|
||||||
</component>
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="128" height="128" version="1.1" viewBox="0 0 33.867 33.867" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g transform="translate(-32.279 -138.64)">
|
|
||||||
<g transform="matrix(.45526 0 0 .45526 33.984 140.17)">
|
|
||||||
<path d="m33.468 66.938c-18.454 0-33.468-15.014-33.468-33.469s15.014-33.469 33.468-33.469c18.455 0 33.469 15.014 33.469 33.469 0 5.621-1.421 11.161-4.116 16.076l4.608 17.2-16.849-4.516c-5.172 3.084-11.069 4.709-17.112 4.709z" fill="#fff"/>
|
|
||||||
<path d="m33.468 67.184c-18.454 0-33.468-15.014-33.468-33.469s15.014-33.469 33.468-33.469c18.455 0 33.469 15.014 33.469 33.469 0 5.621-1.421 11.161-4.116 16.076l4.608 17.2-16.849-4.516c-5.172 3.084-11.069 4.709-17.112 4.709zm0-62.938c-16.249 0-29.468 13.22-29.468 29.469s13.219 29.469 29.468 29.469c5.582 0 11.021-1.574 15.729-4.554l0.74-0.468 11.835 3.171-3.243-12.1 0.419-0.72c2.609-4.484 3.988-9.602 3.988-14.799 0-16.248-13.219-29.468-29.468-29.468z"/>
|
|
||||||
<path d="m25.515 45.296q-2.3937 0-4.2817-0.97772-1.8543-0.97772-2.9332-3.0343-1.0451-2.0566-1.0451-5.2595 0-3.3377 1.1126-5.428 1.1126-2.0903 3.0006-3.068 1.9217-0.97772 4.3492-0.97772 1.3823 0 2.6634 0.30343 1.2812 0.26972 2.0903 0.67429l-0.91029 2.4612q-0.80915-0.30343-1.888-0.57315t-2.0229-0.26972q-5.3269 0-5.3269 6.844 0 3.2703 1.2812 5.0235 1.3149 1.7194 3.8772 1.7194 1.4834 0 2.596-0.30343 1.1463-0.30343 2.0903-0.74172v2.6297q-0.91029 0.472-2.0229 0.708-1.0789 0.26972-2.6297 0.26972zm11.901-0.33714h-2.9669v-25.623h2.9669zm7.2486-24.848q0.67429 0 1.18 0.472 0.53943 0.43829 0.53943 1.416 0 0.94401-0.53943 1.416-0.50572 0.472-1.18 0.472-0.74172 0-1.2474-0.472-0.50572-0.472-0.50572-1.416 0-0.97772 0.50572-1.416 0.50572-0.472 1.2474-0.472zm1.4497 6.7766v18.071h-2.9669v-18.071z" aria-label="cli"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@ -1,46 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Send secure messages to Signal clients
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target
|
|
||||||
Requires=signal-cli-socket.socket
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
CapabilityBoundingSet=
|
|
||||||
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
|
||||||
# Update 'ReadWritePaths' if you change the config path here
|
|
||||||
ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon
|
|
||||||
LockPersonality=true
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateDevices=true
|
|
||||||
PrivateIPC=true
|
|
||||||
PrivateTmp=true
|
|
||||||
PrivateUsers=true
|
|
||||||
ProcSubset=pid
|
|
||||||
ProtectClock=true
|
|
||||||
ProtectControlGroups=true
|
|
||||||
ProtectHome=true
|
|
||||||
ProtectHostname=true
|
|
||||||
ProtectKernelLogs=true
|
|
||||||
ProtectKernelModules=true
|
|
||||||
ProtectKernelTunables=true
|
|
||||||
ProtectProc=invisible
|
|
||||||
ProtectSystem=strict
|
|
||||||
# Profile pictures and attachments to upload must be located here for the service to access them
|
|
||||||
ReadWritePaths=/var/lib/signal-cli
|
|
||||||
RemoveIPC=true
|
|
||||||
RestrictAddressFamilies=AF_INET AF_INET6
|
|
||||||
RestrictNamespaces=true
|
|
||||||
RestrictRealtime=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
StandardInput=socket
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SystemCallArchitectures=native
|
|
||||||
SystemCallFilter=~@debug @mount @obsolete @privileged @resources
|
|
||||||
UMask=0077
|
|
||||||
# Create the user and home directory with 'useradd -r -U -s /usr/sbin/nologin -m -b /var/lib signal-cli'
|
|
||||||
User=signal-cli
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
Also=signal-cli-socket.socket
|
|
||||||
WantedBy=default.target
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Send secure messages to Signal clients
|
|
||||||
|
|
||||||
[Socket]
|
|
||||||
ListenStream=%t/signal-cli/socket
|
|
||||||
SocketUser=root
|
|
||||||
# Add yourself to the signal-cli group to talk with the service
|
|
||||||
# Run 'usermod -aG signal-cli yourusername'
|
|
||||||
SocketGroup=signal-cli
|
|
||||||
SocketMode=0660
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=sockets.target
|
|
||||||
@ -1 +0,0 @@
|
|||||||
u signal-cli - "Signal messaging service" /var/lib/signal-cli
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
d /var/lib/signal-cli 0755 signal-cli signal-cli -
|
|
||||||
d /var/lib/signal-cli/data 0700 signal-cli signal-cli -
|
|
||||||
d /var/lib/signal-cli/attachments 0750 signal-cli signal-cli -
|
|
||||||
d /var/lib/signal-cli/avatars 0750 signal-cli signal-cli -
|
|
||||||
d /var/lib/signal-cli/stickers 0750 signal-cli signal-cli -
|
|
||||||
@ -8,9 +8,9 @@ After=network-online.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=dbus
|
Type=dbus
|
||||||
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
||||||
ExecStart=%dir%/bin/signal-cli -a %I --config /var/lib/signal-cli daemon --dbus-system
|
ExecStart=%dir%/bin/signal-cli -u %I --config /var/lib/signal-cli daemon --system
|
||||||
User=signal-cli
|
User=signal-cli
|
||||||
BusName=org.asamk.Signal
|
BusName=org.asamk.Signal
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
Alias=dbus-org.asamk.Signal.service
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@ -8,7 +8,7 @@ After=network-online.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=dbus
|
Type=dbus
|
||||||
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
||||||
ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon --dbus-system
|
ExecStart=%dir%/bin/signal-cli -u %number% --config /var/lib/signal-cli daemon --system
|
||||||
User=signal-cli
|
User=signal-cli
|
||||||
BusName=org.asamk.Signal
|
BusName=org.asamk.Signal
|
||||||
|
|
||||||
@ -1,359 +0,0 @@
|
|||||||
# Voice Call Support
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
signal-cli supports voice calls by spawning a subprocess called
|
|
||||||
`signal-call-tunnel` for each call. The tunnel handles WebRTC negotiation and
|
|
||||||
audio transport. signal-cli communicates with the tunnel over its stdin/stdout
|
|
||||||
using newline-delimited JSON messages, relaying signaling between the tunnel
|
|
||||||
and the Signal protocol.
|
|
||||||
|
|
||||||
```
|
|
||||||
signal-cli signal-call-tunnel
|
|
||||||
| |
|
|
||||||
|-- spawn --------------------------->|
|
|
||||||
|-- config JSON on stdin ------------>|
|
|
||||||
| |
|
|
||||||
|-- commands on stdin --------------->|
|
|
||||||
|<-- events on stdout ----------------|
|
|
||||||
| | WebRTC
|
|
||||||
| signaling relay | audio I/O
|
|
||||||
| |
|
|
||||||
| (stderr: tunnel logging) -------->| (captured by signal-cli)
|
|
||||||
```
|
|
||||||
|
|
||||||
Each call gets its own tunnel process. When the call ends, signal-cli closes
|
|
||||||
stdin and destroys the process.
|
|
||||||
|
|
||||||
Audio device names (`inputDeviceName`, `outputDeviceName`) are opaque strings
|
|
||||||
returned by the tunnel in its `ready` message. signal-cli passes them through
|
|
||||||
to JSON-RPC clients, which use them to connect audio via platform APIs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Spawning the Tunnel
|
|
||||||
|
|
||||||
For each call, signal-cli:
|
|
||||||
|
|
||||||
1. Spawns `signal-call-tunnel`
|
|
||||||
2. Writes config JSON followed by a newline to stdin
|
|
||||||
3. Keeps stdin open for subsequent control messages
|
|
||||||
4. Reads control events from stdout
|
|
||||||
5. Captures stderr for logging
|
|
||||||
|
|
||||||
The `signal-call-tunnel` binary is located by searching (in order):
|
|
||||||
|
|
||||||
1. `SIGNAL_CALL_TUNNEL_BIN` environment variable
|
|
||||||
2. `<signal-cli install dir>/bin/signal-call-tunnel` (detected from jar location)
|
|
||||||
3. `signal-call-tunnel` on `PATH`
|
|
||||||
|
|
||||||
### Config JSON
|
|
||||||
|
|
||||||
The first line written to the tunnel's stdin:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"call_id": 12345,
|
|
||||||
"is_outgoing": true,
|
|
||||||
"local_device_id": 1,
|
|
||||||
"input_device_name": "signal_input",
|
|
||||||
"output_device_name": "signal_output"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|----------------------|-------------------------|-----------------------------------------------|
|
|
||||||
| `call_id` | unsigned 64-bit integer | Call identifier (use unsigned representation) |
|
|
||||||
| `is_outgoing` | boolean | Whether this is an outgoing call |
|
|
||||||
| `local_device_id` | integer | Signal device ID |
|
|
||||||
| `input_device_name` | string (optional) | Requested input audio device name |
|
|
||||||
| `output_device_name` | string (optional) | Requested output audio device name |
|
|
||||||
|
|
||||||
If `input_device_name` or `output_device_name` are omitted, the tunnel
|
|
||||||
chooses default names. On Linux, these are per-call unique names (e.g.,
|
|
||||||
`signal_input_<call_id>`). On macOS, these are the fixed names `signal_input`
|
|
||||||
and `signal_output`, which must match the pre-installed BlackHole drivers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Control Protocol
|
|
||||||
|
|
||||||
Newline-delimited JSON messages over stdin (signal-cli to tunnel) and stdout
|
|
||||||
(tunnel to signal-cli). The first line on stdin is the config JSON. Subsequent
|
|
||||||
lines are control messages.
|
|
||||||
|
|
||||||
### signal-cli -> Tunnel (stdin)
|
|
||||||
|
|
||||||
| Type | When | Fields |
|
|
||||||
|----------------------|----------------------------|---------------------------------------------------------------------------------------------------|
|
|
||||||
| `createOutgoingCall` | Outgoing call setup | `callId`, `peerId` |
|
|
||||||
| `proceed` | After offer/receivedOffer | `callId`, `hideIp`, `iceServers` |
|
|
||||||
| `receivedOffer` | Incoming call | `callId`, `peerId`, `opaque`, `age`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
|
|
||||||
| `receivedAnswer` | Outgoing call answered | `opaque`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
|
|
||||||
| `receivedIce` | ICE candidates arrive | `candidates` (array of base64 opaque blobs) |
|
|
||||||
| `accept` | User accepts incoming call | *(none)* |
|
|
||||||
| `hangup` | End the call | *(none)* |
|
|
||||||
|
|
||||||
### Tunnel -> signal-cli (stdout)
|
|
||||||
|
|
||||||
| Type | When | Fields |
|
|
||||||
|---------------|---------------------------------------------|------------------------------------------------------|
|
|
||||||
| `ready` | Control socket bound, audio devices created | `inputDeviceName`, `outputDeviceName` |
|
|
||||||
| `sendOffer` | Tunnel generated an offer | `callId`, `opaque`, `callMediaType` |
|
|
||||||
| `sendAnswer` | Tunnel generated an answer | `callId`, `opaque` |
|
|
||||||
| `sendIce` | ICE candidates gathered | `callId`, `candidates` (array of `{"opaque":"..."}`) |
|
|
||||||
| `sendHangup` | Tunnel wants to hang up | `callId`, `hangupType` |
|
|
||||||
| `sendBusy` | Line is busy | `callId` |
|
|
||||||
| `stateChange` | Call state transition | `state`, `reason` (optional) |
|
|
||||||
| `error` | Something went wrong | `message` |
|
|
||||||
|
|
||||||
Opaque blobs and identity keys are base64-encoded. ICE servers use the format:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"urls": [
|
|
||||||
"turn:example.com"
|
|
||||||
],
|
|
||||||
"username": "u",
|
|
||||||
"password": "p"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Startup Sequence
|
|
||||||
|
|
||||||
```
|
|
||||||
signal-cli signal-call-tunnel
|
|
||||||
| |
|
|
||||||
|-- spawn process ------------------> |
|
|
||||||
|-- config JSON + newline on stdin ---->|
|
|
||||||
| | parse config
|
|
||||||
| | initialize audio
|
|
||||||
| |
|
|
||||||
|<-------- ready (on stdout) -----------|
|
|
||||||
| {"type":"ready", |
|
|
||||||
| "inputDeviceName":"...", |
|
|
||||||
| "outputDeviceName":"..."} |
|
|
||||||
| |
|
|
||||||
|-- control messages on stdin --------->|
|
|
||||||
|<-- control events on stdout ----------|
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Call Flows
|
|
||||||
|
|
||||||
### Outgoing call
|
|
||||||
|
|
||||||
```
|
|
||||||
signal-cli signal-call-tunnel Remote Phone
|
|
||||||
| | |
|
|
||||||
|-- spawn + config ------->| |
|
|
||||||
|<-- ready ----------------| |
|
|
||||||
|-- createOutgoingCall --->| |
|
|
||||||
|-- proceed (TURN) ------->| |
|
|
||||||
| | create offer |
|
|
||||||
|<-- sendOffer ------------| |
|
|
||||||
|-- offer via Signal -------------------------------->|
|
|
||||||
|<-- answer via Signal -------------------------------|
|
|
||||||
|-- receivedAnswer ------->| (+ identity keys) |
|
|
||||||
|<-- sendIce --------------| |
|
|
||||||
|-- ICE via Signal -------------------------------> |
|
|
||||||
|<-- ICE via Signal -------------------------------- |
|
|
||||||
|-- receivedIce ---------->| |
|
|
||||||
| | ICE connects |
|
|
||||||
|<-- stateChange:Connected | |
|
|
||||||
```
|
|
||||||
|
|
||||||
### Incoming call
|
|
||||||
|
|
||||||
```
|
|
||||||
signal-cli signal-call-tunnel Remote Phone
|
|
||||||
| | |
|
|
||||||
|<-- offer via Signal --------------------------------|
|
|
||||||
|-- spawn + config ------->| |
|
|
||||||
|<-- ready ----------------| |
|
|
||||||
|-- receivedOffer -------->| (+ identity keys) |
|
|
||||||
|-- proceed (TURN) ------->| |
|
|
||||||
| | process offer |
|
|
||||||
|<-- sendAnswer -----------| |
|
|
||||||
|-- answer via Signal -------------------------------->|
|
|
||||||
|<-- sendIce --------------| |
|
|
||||||
|-- ICE via Signal ------------------------------> |
|
|
||||||
|<-- ICE via Signal -------------------------------- |
|
|
||||||
|-- receivedIce ---------->| |
|
|
||||||
| | ICE connecting... |
|
|
||||||
| | |
|
|
||||||
| (user accepts call) | |
|
|
||||||
| Java defers accept | |
|
|
||||||
| | |
|
|
||||||
|<-- stateChange:Ringing --| (tunnel ready to accept)|
|
|
||||||
|-- accept --------------->| (deferred accept sent) |
|
|
||||||
| | accept |
|
|
||||||
|<-- stateChange:Connected | |
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON-RPC client perspective
|
|
||||||
|
|
||||||
An external application (bot, UI, test script) interacts via JSON-RPC only.
|
|
||||||
|
|
||||||
**Important:** Call event notifications are not sent by default. Clients must
|
|
||||||
call `subscribeCallEvents` before initiating or receiving calls. Without this,
|
|
||||||
incoming calls are silently ignored (no tunnel is spawned).
|
|
||||||
|
|
||||||
```
|
|
||||||
JSON-RPC Client signal-cli daemon
|
|
||||||
| |
|
|
||||||
|-- subscribeCallEvents() ------------>| (required: enables call support)
|
|
||||||
| |
|
|
||||||
|-- startCall(recipient) ------------->|
|
|
||||||
|<-- {callId, state, -|
|
|
||||||
| inputDeviceName, |
|
|
||||||
| outputDeviceName} |
|
|
||||||
| |
|
|
||||||
|<-- callEvent: RINGING_OUTGOING ------|
|
|
||||||
| ... remote answers ... |
|
|
||||||
|<-- callEvent: CONNECTED -------------|
|
|
||||||
| |
|
|
||||||
| connect to audio devices |
|
|
||||||
| (via platform audio APIs) |
|
|
||||||
| |
|
|
||||||
|-- hangupCall(callId) --------------->| (or: receive callEvent ENDED)
|
|
||||||
|<-- callEvent: ENDED -----------------|
|
|
||||||
| disconnect from audio devices |
|
|
||||||
```
|
|
||||||
|
|
||||||
For incoming calls:
|
|
||||||
|
|
||||||
```
|
|
||||||
JSON-RPC Client signal-cli daemon
|
|
||||||
| |
|
|
||||||
|-- subscribeCallEvents() ------------>| (if not already subscribed)
|
|
||||||
| |
|
|
||||||
|<-- callEvent: RINGING_INCOMING ------| (includes callId, device names)
|
|
||||||
| |
|
|
||||||
|-- acceptCall(callId) --------------->|
|
|
||||||
|<-- {callId, state, -|
|
|
||||||
| inputDeviceName, |
|
|
||||||
| outputDeviceName} |
|
|
||||||
| |
|
|
||||||
|<-- callEvent: CONNECTING ------------|
|
|
||||||
|<-- callEvent: CONNECTED -------------|
|
|
||||||
| |
|
|
||||||
| connect to audio devices |
|
|
||||||
| (via platform audio APIs) |
|
|
||||||
```
|
|
||||||
|
|
||||||
To stop receiving call events, call `unsubscribeCallEvents`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## State Machine
|
|
||||||
|
|
||||||
Call states as seen by JSON-RPC clients:
|
|
||||||
|
|
||||||
```
|
|
||||||
startCall()
|
|
||||||
|
|
|
||||||
v
|
|
||||||
+----- RINGING_OUTGOING ----+ RINGING_INCOMING -----+
|
|
||||||
| | | | |
|
|
||||||
| (timeout | (answered) | (rejected) | acceptCall() | (timeout
|
|
||||||
| ~60s) | | | | ~60s)
|
|
||||||
v v v v v
|
|
||||||
ENDED CONNECTED ENDED CONNECTING ENDED
|
|
||||||
| |
|
|
||||||
| v
|
|
||||||
| CONNECTED
|
|
||||||
| |
|
|
||||||
| (hangup/error) | (hangup/error)
|
|
||||||
v v
|
|
||||||
ENDED ENDED
|
|
||||||
```
|
|
||||||
|
|
||||||
For outgoing calls, `CONNECTED` fires directly when the tunnel reports
|
|
||||||
`Connected` state -- there is no intermediate `CONNECTING` event.
|
|
||||||
|
|
||||||
For incoming calls, `CONNECTING` is set by Java when the user calls
|
|
||||||
`acceptCall()`, before the tunnel completes ICE negotiation.
|
|
||||||
|
|
||||||
Both directions have a 60-second ring timeout.
|
|
||||||
|
|
||||||
Reconnection (ICE restart):
|
|
||||||
|
|
||||||
```
|
|
||||||
CONNECTED --> RECONNECTING --> CONNECTED (ICE restart succeeded)
|
|
||||||
|
|
|
||||||
v
|
|
||||||
ENDED (ICE restart failed)
|
|
||||||
```
|
|
||||||
|
|
||||||
`RECONNECTING` maps from the tunnel's `Connecting` state, which is emitted
|
|
||||||
during ICE restarts (not during initial connection).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CallManager.java
|
|
||||||
|
|
||||||
`lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java`
|
|
||||||
|
|
||||||
Manages the call lifecycle from the Java side:
|
|
||||||
|
|
||||||
1. Spawns `signal-call-tunnel` and writes config JSON to stdin
|
|
||||||
2. Keeps stdin open as the control write channel; reads stdout for control events
|
|
||||||
3. Captures stderr for tunnel logging
|
|
||||||
4. Parses `inputDeviceName` and `outputDeviceName` from the tunnel's `ready`
|
|
||||||
message and includes them in `CallInfo`
|
|
||||||
5. Translates tunnel state changes into `CallInfo.State` values and fires
|
|
||||||
`callEvent` JSON-RPC notifications to connected clients
|
|
||||||
6. Defers the `accept` message for incoming calls until the tunnel reports
|
|
||||||
`Ringing` state (sending earlier causes the tunnel to drop it)
|
|
||||||
7. Schedules a 60-second ring timeout for both incoming and outgoing calls
|
|
||||||
8. On hangup: sends hangup message, closes stdin, and destroys the process
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
### Peer ID consistency
|
|
||||||
|
|
||||||
The `peerId` field in `createOutgoingCall` and `receivedOffer` must be the actual
|
|
||||||
remote peer UUID (e.g., `senderAddress.toString()`). The tunnel rejects ICE
|
|
||||||
candidates if the peer ID doesn't match across calls, causing "Ignoring
|
|
||||||
peer-reflexive ICE candidate because the ufrag is unknown."
|
|
||||||
|
|
||||||
### sendHangup semantics
|
|
||||||
|
|
||||||
`sendHangup` from the tunnel is a request to send a hangup message via Signal
|
|
||||||
protocol. It is **not** a local state change -- local state transitions come
|
|
||||||
exclusively from `stateChange` events. For single-device clients, ignore
|
|
||||||
`AcceptedOnAnotherDevice`, `DeclinedOnAnotherDevice`, and
|
|
||||||
`BusyOnAnotherDevice` hangup types in the `hangupType` field -- sending these to
|
|
||||||
the remote peer causes it to terminate the call prematurely.
|
|
||||||
|
|
||||||
### Call ID serialization
|
|
||||||
|
|
||||||
Call IDs can exceed `Long.MAX_VALUE` in Java. Use `Long.toUnsignedString()` when
|
|
||||||
serializing to JSON for the tunnel (which expects unsigned 64-bit integers). In
|
|
||||||
the config JSON, `call_id` should also use unsigned representation.
|
|
||||||
|
|
||||||
### Incoming hangup filtering
|
|
||||||
|
|
||||||
When receiving hangup messages via Signal protocol, only honor `NORMAL` type
|
|
||||||
hangups. `ACCEPTED`, `DECLINED`, and `BUSY` types are multi-device coordination
|
|
||||||
messages and should be ignored by single-device clients.
|
|
||||||
|
|
||||||
### JSON-RPC call ID types
|
|
||||||
|
|
||||||
JSON-RPC clients may send call IDs as various numeric types (Long, BigInteger,
|
|
||||||
Integer). Use `Number.longValue()` rather than direct casting when extracting
|
|
||||||
call IDs from JSON-RPC parameters.
|
|
||||||
|
|
||||||
### Identity key format
|
|
||||||
|
|
||||||
Identity keys in `senderIdentityKey` and `receiverIdentityKey` must be **raw
|
|
||||||
32-byte Curve25519 public keys** (without the 0x05 DJB type prefix). If the
|
|
||||||
33-byte serialized form is used instead, SRTP key derivation produces different
|
|
||||||
keys on each side, causing authentication failures.
|
|
||||||
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
[versions]
|
|
||||||
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_148"
|
|
||||||
|
|
||||||
[libraries]
|
|
||||||
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"
|
|
||||||
|
|
||||||
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" }
|
|
||||||
junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
7
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,9 +1,6 @@
|
|||||||
|
#Mon Nov 14 16:58:51 CET 2016
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
retries=0
|
|
||||||
retryBackOffMs=500
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-3.2-bin.zip
|
||||||
|
|||||||
311
gradlew
vendored
311
gradlew
vendored
@ -1,128 +1,78 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
##
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
## Gradle start up script for UN*X
|
||||||
#
|
##
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
app_path=$0
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
# Need this for daisy-chained symlinks.
|
while [ -h "$PRG" ] ; do
|
||||||
while
|
ls=`ls -ld "$PRG"`
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
[ -h "$app_path" ]
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
do
|
PRG="$link"
|
||||||
ls=$( ls -ld "$app_path" )
|
else
|
||||||
link=${ls#*' -> '}
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
case $link in #(
|
fi
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
# This is normally unused
|
APP_NAME="Gradle"
|
||||||
# shellcheck disable=SC2034
|
APP_BASE_NAME=`basename "$0"`
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD="maximum"
|
||||||
|
|
||||||
warn () {
|
warn ( ) {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
} >&2
|
}
|
||||||
|
|
||||||
die () {
|
die ( ) {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
} >&2
|
}
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "$( uname )" in #(
|
case "`uname`" in
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
CYGWIN* )
|
||||||
Darwin* ) darwin=true ;; #(
|
cygwin=true
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
;;
|
||||||
NONSTOP* ) nonstop=true ;;
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
else
|
else
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
@ -131,118 +81,91 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD="java"
|
||||||
if ! command -v java >/dev/null 2>&1
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
case $MAX_FD in #(
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
max*)
|
if [ $? -eq 0 ] ; then
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
# shellcheck disable=SC2039,SC3045
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
fi
|
||||||
# Roll the args list around exactly as many times as the number of
|
ulimit -n $MAX_FD
|
||||||
# args, so each arg winds up back in the position where it started, but
|
if [ $? -ne 0 ] ; then
|
||||||
# possibly modified.
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
#
|
fi
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
else
|
||||||
# changing the positional parameters here affects neither the number of
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
# iterations, nor the values presented in `arg`.
|
fi
|
||||||
shift # remove old arg
|
fi
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
done
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
for s in "${@}" ; do
|
||||||
|
s=\"$s\"
|
||||||
|
APP_ARGS=$APP_ARGS" "$s
|
||||||
|
done
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
eval set -- "$DEFAULT_JVM_OPTS" "$JAVA_OPTS" "$GRADLE_OPTS" "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
# and any embedded shellness will be escaped.
|
cd "$(dirname "$0")"
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|||||||
98
gradlew.bat
vendored
98
gradlew.bat
vendored
@ -1,82 +1,84 @@
|
|||||||
@rem
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
@rem Set local scope for the variables with windows NT shell
|
||||||
setlocal EnableExtensions
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
"%COMSPEC%" /c exit 1
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
"%COMSPEC%" /c exit 1
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
@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
|
|
||||||
|
|
||||||
:exitWithErrorLevel
|
:end
|
||||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
@rem End local scope for the variables with windows NT shell
|
||||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
if "%ERRORLEVEL%"=="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!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
plugins {
|
|
||||||
`java-library`
|
|
||||||
`check-lib-versions`
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_25
|
|
||||||
targetCompatibility = JavaVersion.VERSION_25
|
|
||||||
|
|
||||||
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
|
||||||
toolchain {
|
|
||||||
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString()
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
if (libsignalClientPath == null) {
|
|
||||||
implementation(libs.signalnetwork)
|
|
||||||
} else {
|
|
||||||
implementation(libs.signalnetwork) {
|
|
||||||
exclude(group = "org.signal", module = "libsignal-client")
|
|
||||||
}
|
|
||||||
implementation(files(libsignalClientPath))
|
|
||||||
}
|
|
||||||
implementation(libs.jackson.databind)
|
|
||||||
implementation(libs.bouncycastle)
|
|
||||||
implementation(libs.slf4j.api)
|
|
||||||
implementation(libs.sqlite)
|
|
||||||
implementation(libs.hikari)
|
|
||||||
|
|
||||||
testImplementation(libs.junit.jupiter)
|
|
||||||
testImplementation(platform(libs.junit.jupiter.bom))
|
|
||||||
testRuntimeOnly(libs.junit.launcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named<Test>("test") {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
implementation {
|
|
||||||
resolutionStrategy.failOnVersionConflict()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<AbstractArchiveTask>().configureEach {
|
|
||||||
isPreserveFileTimestamps = false
|
|
||||||
isReproducibleFileOrder = true
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<JavaCompile> {
|
|
||||||
options.encoding = "UTF-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.jar {
|
|
||||||
manifest {
|
|
||||||
attributes("Automatic-Module-Name" to "org.asamk.signal.manager")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,478 +0,0 @@
|
|||||||
package org.asamk.signal.manager;
|
|
||||||
|
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
|
||||||
import org.asamk.signal.manager.api.CallInfo;
|
|
||||||
import org.asamk.signal.manager.api.CallOffer;
|
|
||||||
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
|
||||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
|
||||||
import org.asamk.signal.manager.api.Configuration;
|
|
||||||
import org.asamk.signal.manager.api.Device;
|
|
||||||
import org.asamk.signal.manager.api.DeviceLimitExceededException;
|
|
||||||
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.GroupNotFoundException;
|
|
||||||
import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
|
||||||
import org.asamk.signal.manager.api.Identity;
|
|
||||||
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
|
||||||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
|
||||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
|
||||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
|
||||||
import org.asamk.signal.manager.api.InvalidStickerException;
|
|
||||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
|
||||||
import org.asamk.signal.manager.api.LastGroupAdminException;
|
|
||||||
import org.asamk.signal.manager.api.Message;
|
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
|
||||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
|
||||||
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
|
||||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
|
||||||
import org.asamk.signal.manager.api.Pair;
|
|
||||||
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
|
||||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
|
||||||
import org.asamk.signal.manager.api.PinLockedException;
|
|
||||||
import org.asamk.signal.manager.api.RateLimitException;
|
|
||||||
import org.asamk.signal.manager.api.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.StickerPackInvalidException;
|
|
||||||
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.UnregisteredRecipientException;
|
|
||||||
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.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public interface Manager extends Closeable {
|
|
||||||
|
|
||||||
static boolean isValidNumber(final String e164Number, final String countryCode) {
|
|
||||||
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isSignalClientAvailable() {
|
|
||||||
final Logger logger = LoggerFactory.getLogger(Manager.class);
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
org.signal.libsignal.internal.Native.UuidCiphertext_CheckValidContents(new byte[0]);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.trace("Expected exception when checking libsignal-client: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (UnsatisfiedLinkError e) {
|
|
||||||
logger.warn("Failed to call libsignal-client: {}", e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String getSelfNumber();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is used for checking a set of phone numbers for registration on Signal
|
|
||||||
*
|
|
||||||
* @param numbers The set of phone number in question
|
|
||||||
* @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
|
|
||||||
* @throws IOException if it's unable to get the contacts to check if they're registered
|
|
||||||
*/
|
|
||||||
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
|
|
||||||
|
|
||||||
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
|
|
||||||
|
|
||||||
void updateAccountAttributes(
|
|
||||||
String deviceName,
|
|
||||||
Boolean unrestrictedUnidentifiedSender,
|
|
||||||
final Boolean discoverableByNumber,
|
|
||||||
final Boolean numberSharing
|
|
||||||
) throws IOException;
|
|
||||||
|
|
||||||
Configuration getConfiguration();
|
|
||||||
|
|
||||||
void updateConfiguration(Configuration configuration) throws NotPrimaryDeviceException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the user's profile.
|
|
||||||
* If a field is null, the previous value will be kept.
|
|
||||||
*/
|
|
||||||
void updateProfile(UpdateProfile updateProfile) throws IOException;
|
|
||||||
|
|
||||||
String getUsername();
|
|
||||||
|
|
||||||
UsernameLinkUrl getUsernameLink();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a username for the account.
|
|
||||||
* If the username is null, it will be deleted.
|
|
||||||
*/
|
|
||||||
void setUsername(String username) throws IOException, InvalidUsernameException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a username for the account.
|
|
||||||
* If the username is null, it will be deleted.
|
|
||||||
*/
|
|
||||||
void deleteUsername() throws IOException;
|
|
||||||
|
|
||||||
void startChangeNumber(
|
|
||||||
String newNumber,
|
|
||||||
boolean voiceVerification,
|
|
||||||
String captcha
|
|
||||||
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException;
|
|
||||||
|
|
||||||
void finishChangeNumber(
|
|
||||||
String newNumber,
|
|
||||||
String verificationCode,
|
|
||||||
String pin
|
|
||||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
|
|
||||||
|
|
||||||
void unregister() throws IOException;
|
|
||||||
|
|
||||||
void deleteAccount() throws IOException;
|
|
||||||
|
|
||||||
void submitRateLimitRecaptchaChallenge(
|
|
||||||
String challenge,
|
|
||||||
String captcha
|
|
||||||
) throws IOException, CaptchaRejectedException;
|
|
||||||
|
|
||||||
List<Device> getLinkedDevices() throws IOException;
|
|
||||||
|
|
||||||
void updateLinkedDevice(int deviceId, String name) throws IOException, NotPrimaryDeviceException;
|
|
||||||
|
|
||||||
void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException;
|
|
||||||
|
|
||||||
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException;
|
|
||||||
|
|
||||||
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException;
|
|
||||||
|
|
||||||
List<Group> getGroups();
|
|
||||||
|
|
||||||
List<Group> getGroups(Collection<GroupId> groupIds);
|
|
||||||
|
|
||||||
SendGroupMessageResults quitGroup(
|
|
||||||
GroupId groupId,
|
|
||||||
Set<RecipientIdentifier.Single> groupAdmins
|
|
||||||
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void deleteGroup(GroupId groupId) throws IOException;
|
|
||||||
|
|
||||||
Pair<GroupId, SendGroupMessageResults> createGroup(
|
|
||||||
String name,
|
|
||||||
Set<RecipientIdentifier.Single> members,
|
|
||||||
String avatarFile
|
|
||||||
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
SendGroupMessageResults updateGroup(
|
|
||||||
final GroupId groupId,
|
|
||||||
final UpdateGroup updateGroup
|
|
||||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
Pair<GroupId, SendGroupMessageResults> joinGroup(
|
|
||||||
GroupInviteLinkUrl inviteLinkUrl
|
|
||||||
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
|
|
||||||
|
|
||||||
SendMessageResults sendTypingMessage(
|
|
||||||
TypingAction action,
|
|
||||||
Set<RecipientIdentifier> recipients
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
|
||||||
|
|
||||||
SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
|
|
||||||
|
|
||||||
SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
|
|
||||||
|
|
||||||
SendMessageResults sendMessage(
|
|
||||||
Message message,
|
|
||||||
Set<RecipientIdentifier> recipients,
|
|
||||||
boolean notifySelf
|
|
||||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
|
||||||
|
|
||||||
SendMessageResults sendEditMessage(
|
|
||||||
Message message,
|
|
||||||
Set<RecipientIdentifier> recipients,
|
|
||||||
long editTargetTimestamp
|
|
||||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
|
||||||
|
|
||||||
SendMessageResults sendRemoteDeleteMessage(
|
|
||||||
long targetSentTimestamp,
|
|
||||||
Set<RecipientIdentifier> recipients
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
|
||||||
|
|
||||||
SendMessageResults sendMessageReaction(
|
|
||||||
String emoji,
|
|
||||||
boolean remove,
|
|
||||||
RecipientIdentifier.Single targetAuthor,
|
|
||||||
long targetSentTimestamp,
|
|
||||||
Set<RecipientIdentifier> recipients,
|
|
||||||
final boolean notifySelf,
|
|
||||||
final boolean isStory
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
SendMessageResults sendAdminDelete(
|
|
||||||
RecipientIdentifier.Single targetAuthor,
|
|
||||||
long targetSentTimestamp,
|
|
||||||
Set<RecipientIdentifier.Group> recipients,
|
|
||||||
boolean notifySelf,
|
|
||||||
boolean isStory
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
SendMessageResults sendPinMessage(
|
|
||||||
int pinDuration,
|
|
||||||
RecipientIdentifier.Single targetAuthor,
|
|
||||||
long targetSentTimestamp,
|
|
||||||
Set<RecipientIdentifier> recipients,
|
|
||||||
boolean notifySelf,
|
|
||||||
boolean isStory
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
SendMessageResults sendUnpinMessage(
|
|
||||||
RecipientIdentifier.Single targetAuthor,
|
|
||||||
long targetSentTimestamp,
|
|
||||||
Set<RecipientIdentifier> recipients,
|
|
||||||
boolean notifySelf,
|
|
||||||
boolean isStory
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
SendMessageResults sendPaymentNotificationMessage(
|
|
||||||
byte[] receipt,
|
|
||||||
String note,
|
|
||||||
RecipientIdentifier.Single recipient
|
|
||||||
) throws IOException;
|
|
||||||
|
|
||||||
void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
|
||||||
|
|
||||||
SendMessageResults sendMessageRequestResponse(
|
|
||||||
MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
|
||||||
Set<RecipientIdentifier> recipientIdentifiers
|
|
||||||
);
|
|
||||||
|
|
||||||
SendMessageResults sendPollCreateMessage(
|
|
||||||
final String question,
|
|
||||||
final boolean allowMultiple,
|
|
||||||
final List<String> options,
|
|
||||||
final Set<RecipientIdentifier> recipients,
|
|
||||||
final boolean notifySelf
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
SendMessageResults sendPollVoteMessage(
|
|
||||||
final RecipientIdentifier.Single targetAuthor,
|
|
||||||
final long targetSentTimestamp,
|
|
||||||
final List<Integer> optionIndexes,
|
|
||||||
final int voteCount,
|
|
||||||
final Set<RecipientIdentifier> recipients,
|
|
||||||
final boolean notifySelf
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
SendMessageResults sendPollTerminateMessage(
|
|
||||||
final long targetSentTimestamp,
|
|
||||||
final Set<RecipientIdentifier> recipients,
|
|
||||||
final boolean notifySelf
|
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void hideRecipient(RecipientIdentifier.Single recipient);
|
|
||||||
|
|
||||||
void deleteRecipient(RecipientIdentifier.Single recipient);
|
|
||||||
|
|
||||||
void deleteContact(RecipientIdentifier.Single recipient);
|
|
||||||
|
|
||||||
void setContactName(
|
|
||||||
final RecipientIdentifier.Single recipient,
|
|
||||||
final String givenName,
|
|
||||||
final String familyName,
|
|
||||||
final String nickGivenName,
|
|
||||||
final String nickFamilyName,
|
|
||||||
final String note
|
|
||||||
) throws UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void setContactsBlocked(
|
|
||||||
Collection<RecipientIdentifier.Single> recipient,
|
|
||||||
boolean blocked
|
|
||||||
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void setGroupsBlocked(
|
|
||||||
Collection<GroupId> groupId,
|
|
||||||
boolean blocked
|
|
||||||
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the expiration timer for a contact
|
|
||||||
*/
|
|
||||||
void setExpirationTimer(
|
|
||||||
RecipientIdentifier.Single recipient,
|
|
||||||
int messageExpirationTimer
|
|
||||||
) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload the sticker pack from path.
|
|
||||||
*
|
|
||||||
* @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
|
|
||||||
* @return if successful, returns the URL to install the sticker pack in the signal app
|
|
||||||
*/
|
|
||||||
StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException;
|
|
||||||
|
|
||||||
void installStickerPack(StickerPackUrl url) throws IOException;
|
|
||||||
|
|
||||||
List<StickerPack> getStickerPacks();
|
|
||||||
|
|
||||||
void requestAllSyncData() throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a handler to receive new messages.
|
|
||||||
* Will start receiving messages from server, if not already started.
|
|
||||||
*/
|
|
||||||
default void addReceiveHandler(ReceiveMessageHandler handler) {
|
|
||||||
addReceiveHandler(handler, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addReceiveHandler(ReceiveMessageHandler handler, final boolean isWeakListener);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a handler to receive new messages.
|
|
||||||
* Will stop receiving messages from server, if this was the last registered receiver.
|
|
||||||
*/
|
|
||||||
void removeReceiveHandler(ReceiveMessageHandler handler);
|
|
||||||
|
|
||||||
boolean isReceiving();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receive new messages from server, returns if no new message arrive in a timespan of timeout.
|
|
||||||
*/
|
|
||||||
void receiveMessages(
|
|
||||||
Optional<Duration> timeout,
|
|
||||||
Optional<Integer> maxMessages,
|
|
||||||
ReceiveMessageHandler handler
|
|
||||||
) throws IOException, AlreadyReceivingException;
|
|
||||||
|
|
||||||
void stopReceiveMessages();
|
|
||||||
|
|
||||||
void setReceiveConfig(ReceiveConfig receiveConfig);
|
|
||||||
|
|
||||||
boolean isContactBlocked(RecipientIdentifier.Single recipient);
|
|
||||||
|
|
||||||
void sendContacts() throws IOException;
|
|
||||||
|
|
||||||
List<Recipient> getRecipients(
|
|
||||||
boolean onlyContacts,
|
|
||||||
Optional<Boolean> blocked,
|
|
||||||
Collection<RecipientIdentifier.Single> address,
|
|
||||||
Optional<String> name
|
|
||||||
);
|
|
||||||
|
|
||||||
String getContactOrProfileName(RecipientIdentifier.Single recipient);
|
|
||||||
|
|
||||||
Group getGroup(GroupId groupId);
|
|
||||||
|
|
||||||
List<Identity> getIdentities();
|
|
||||||
|
|
||||||
List<Identity> getIdentities(RecipientIdentifier.Single recipient);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trust this the identity with this fingerprint/safetyNumber
|
|
||||||
*
|
|
||||||
* @param recipient account of the identity
|
|
||||||
*/
|
|
||||||
boolean trustIdentityVerified(
|
|
||||||
RecipientIdentifier.Single recipient,
|
|
||||||
IdentityVerificationCode verificationCode
|
|
||||||
) throws UnregisteredRecipientException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trust all keys of this identity without verification
|
|
||||||
*
|
|
||||||
* @param recipient account of the identity
|
|
||||||
*/
|
|
||||||
boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void addAddressChangedListener(Runnable listener);
|
|
||||||
|
|
||||||
void addClosedListener(Runnable listener);
|
|
||||||
|
|
||||||
InputStream retrieveAttachment(final String id) throws IOException;
|
|
||||||
|
|
||||||
InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException;
|
|
||||||
|
|
||||||
InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException;
|
|
||||||
|
|
||||||
// --- Voice call methods ---
|
|
||||||
|
|
||||||
CallInfo startCall(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
CallInfo acceptCall(long callId) throws IOException;
|
|
||||||
|
|
||||||
void hangupCall(long callId) throws IOException;
|
|
||||||
|
|
||||||
SendMessageResult rejectCall(long callId) throws IOException;
|
|
||||||
|
|
||||||
List<CallInfo> listActiveCalls();
|
|
||||||
|
|
||||||
void sendCallOffer(
|
|
||||||
RecipientIdentifier.Single recipient,
|
|
||||||
CallOffer offer
|
|
||||||
) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void sendCallAnswer(
|
|
||||||
RecipientIdentifier.Single recipient,
|
|
||||||
long callId,
|
|
||||||
byte[] answerOpaque
|
|
||||||
) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void sendIceUpdate(
|
|
||||||
RecipientIdentifier.Single recipient,
|
|
||||||
long callId,
|
|
||||||
List<byte[]> iceCandidates
|
|
||||||
) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void sendHangup(
|
|
||||||
RecipientIdentifier.Single recipient,
|
|
||||||
long callId,
|
|
||||||
MessageEnvelope.Call.Hangup.Type type
|
|
||||||
) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
void sendBusy(RecipientIdentifier.Single recipient, long callId) throws IOException, UnregisteredRecipientException;
|
|
||||||
|
|
||||||
List<TurnServer> getTurnServerInfo() throws IOException;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void close();
|
|
||||||
|
|
||||||
void addCallEventListener(CallEventListener listener);
|
|
||||||
|
|
||||||
void removeCallEventListener(CallEventListener listener);
|
|
||||||
|
|
||||||
interface ReceiveMessageHandler {
|
|
||||||
|
|
||||||
ReceiveMessageHandler EMPTY = (envelope, e) -> {
|
|
||||||
};
|
|
||||||
|
|
||||||
void handleMessage(MessageEnvelope envelope, Throwable e);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallEventListener {
|
|
||||||
|
|
||||||
void handleCallEvent(CallInfo callInfo, String reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.internal.LibSignalLogger;
|
|
||||||
import org.asamk.signal.manager.internal.SignalLogger;
|
|
||||||
|
|
||||||
public class ManagerLogger {
|
|
||||||
|
|
||||||
public static void initLogger() {
|
|
||||||
LibSignalLogger.initLogger();
|
|
||||||
SignalLogger.initLogger();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
package org.asamk.signal.manager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public interface MultiAccountManager extends AutoCloseable {
|
|
||||||
|
|
||||||
List<String> getAccountNumbers();
|
|
||||||
|
|
||||||
List<Manager> getManagers();
|
|
||||||
|
|
||||||
void addOnManagerAddedHandler(Consumer<Manager> handler);
|
|
||||||
|
|
||||||
void addOnManagerRemovedHandler(Consumer<Manager> handler);
|
|
||||||
|
|
||||||
Manager getManager(String phoneNumber);
|
|
||||||
|
|
||||||
URI getNewProvisioningDeviceLinkUri() throws TimeoutException, IOException;
|
|
||||||
|
|
||||||
ProvisioningManager getProvisioningManagerFor(URI deviceLinkUri);
|
|
||||||
|
|
||||||
RegistrationManager getNewRegistrationManager(String account) throws IOException;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void close();
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package org.asamk.signal.manager;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.UserAlreadyExistsException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
public interface ProvisioningManager {
|
|
||||||
|
|
||||||
URI getDeviceLinkUri() throws TimeoutException, IOException;
|
|
||||||
|
|
||||||
String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException;
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
package org.asamk.signal.manager;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
|
||||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
|
||||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
|
||||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
|
||||||
import org.asamk.signal.manager.api.PinLockedException;
|
|
||||||
import org.asamk.signal.manager.api.RateLimitException;
|
|
||||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public interface RegistrationManager extends Closeable {
|
|
||||||
|
|
||||||
void register(
|
|
||||||
boolean voiceVerification,
|
|
||||||
String captcha,
|
|
||||||
final boolean forceRegister
|
|
||||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
|
|
||||||
|
|
||||||
void verifyAccount(
|
|
||||||
String verificationCode,
|
|
||||||
String pin
|
|
||||||
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
|
|
||||||
|
|
||||||
void deleteLocalAccountData() throws IOException;
|
|
||||||
|
|
||||||
boolean isRegistered();
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package org.asamk.signal.manager;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.TrustNewIdentity;
|
|
||||||
|
|
||||||
public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) {
|
|
||||||
|
|
||||||
public static final Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false);
|
|
||||||
}
|
|
||||||
@ -1,209 +0,0 @@
|
|||||||
package org.asamk.signal.manager;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.AccountCheckException;
|
|
||||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
|
||||||
import org.asamk.signal.manager.api.Pair;
|
|
||||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
|
||||||
import org.asamk.signal.manager.config.ServiceConfig;
|
|
||||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
|
||||||
import org.asamk.signal.manager.internal.AccountFileUpdaterImpl;
|
|
||||||
import org.asamk.signal.manager.internal.ManagerImpl;
|
|
||||||
import org.asamk.signal.manager.internal.MultiAccountManagerImpl;
|
|
||||||
import org.asamk.signal.manager.internal.PathConfig;
|
|
||||||
import org.asamk.signal.manager.internal.ProvisioningManagerImpl;
|
|
||||||
import org.asamk.signal.manager.internal.RegistrationManagerImpl;
|
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
|
||||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class SignalAccountFiles {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MultiAccountManager.class);
|
|
||||||
|
|
||||||
private final PathConfig pathConfig;
|
|
||||||
private final ServiceEnvironment serviceEnvironment;
|
|
||||||
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
|
|
||||||
private final String userAgent;
|
|
||||||
private final Settings settings;
|
|
||||||
private final AccountsStore accountsStore;
|
|
||||||
|
|
||||||
public SignalAccountFiles(
|
|
||||||
final File settingsPath,
|
|
||||||
final ServiceEnvironment serviceEnvironment,
|
|
||||||
final String userAgent,
|
|
||||||
final Settings settings
|
|
||||||
) throws IOException {
|
|
||||||
this.pathConfig = PathConfig.createDefault(settingsPath);
|
|
||||||
this.serviceEnvironment = serviceEnvironment;
|
|
||||||
this.serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(this.serviceEnvironment, userAgent);
|
|
||||||
this.userAgent = userAgent;
|
|
||||||
this.settings = settings;
|
|
||||||
this.accountsStore = new AccountsStore(pathConfig.dataPath(), serviceEnvironment, accountPath -> {
|
|
||||||
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return SignalAccount.load(pathConfig.dataPath(), accountPath, false, settings);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getAllLocalAccountNumbers() throws IOException {
|
|
||||||
return accountsStore.getAllNumbers();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
|
||||||
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
|
||||||
try {
|
|
||||||
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
|
|
||||||
} catch (NotRegisteredException e) {
|
|
||||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
|
||||||
return null;
|
|
||||||
} catch (AccountCheckException | IOException e) {
|
|
||||||
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
|
||||||
return new Pair<Manager, Throwable>(null, e);
|
|
||||||
}
|
|
||||||
}).filter(Objects::nonNull).toList();
|
|
||||||
|
|
||||||
for (final var pair : managerPairs) {
|
|
||||||
if (pair.second() instanceof IOException e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final var managers = managerPairs.stream()
|
|
||||||
.filter(p -> p != null && p.first() != null)
|
|
||||||
.map(Pair::first)
|
|
||||||
.toList();
|
|
||||||
return new MultiAccountManagerImpl(managers, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
|
||||||
final var accountPath = accountsStore.getPathByNumber(number);
|
|
||||||
return this.initManager(number, accountPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manager initManager(
|
|
||||||
String number,
|
|
||||||
String accountPath
|
|
||||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
|
||||||
if (accountPath == null) {
|
|
||||||
throw new NotRegisteredException();
|
|
||||||
}
|
|
||||||
if (!SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
|
||||||
throw new NotRegisteredException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
|
||||||
if (!number.equals(account.getNumber())) {
|
|
||||||
account.close();
|
|
||||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!account.isRegistered()) {
|
|
||||||
account.close();
|
|
||||||
throw new NotRegisteredException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.getServiceEnvironment() != null && account.getServiceEnvironment() != serviceEnvironment) {
|
|
||||||
throw new IOException("Account is registered in another environment: " + account.getServiceEnvironment());
|
|
||||||
}
|
|
||||||
|
|
||||||
account.initDatabase();
|
|
||||||
|
|
||||||
final var manager = new ManagerImpl(account,
|
|
||||||
pathConfig,
|
|
||||||
new AccountFileUpdaterImpl(accountsStore, accountPath),
|
|
||||||
serviceEnvironmentConfig,
|
|
||||||
userAgent);
|
|
||||||
|
|
||||||
try {
|
|
||||||
manager.checkAccountState();
|
|
||||||
} catch (DeprecatedVersionException e) {
|
|
||||||
manager.close();
|
|
||||||
throw new IOException("signal-cli version is too old for the Signal-Server, please update.");
|
|
||||||
} catch (IOException e) {
|
|
||||||
manager.close();
|
|
||||||
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.getServiceEnvironment() == null) {
|
|
||||||
account.setServiceEnvironment(serviceEnvironment);
|
|
||||||
accountsStore.updateAccount(accountPath, account.getNumber(), account.getAci());
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProvisioningManager initProvisioningManager() {
|
|
||||||
return initProvisioningManager(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProvisioningManager initProvisioningManager(Consumer<Manager> newManagerListener) {
|
|
||||||
return new ProvisioningManagerImpl(pathConfig,
|
|
||||||
serviceEnvironmentConfig,
|
|
||||||
userAgent,
|
|
||||||
newManagerListener,
|
|
||||||
accountsStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RegistrationManager initRegistrationManager(String number) throws IOException {
|
|
||||||
return initRegistrationManager(number, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RegistrationManager initRegistrationManager(
|
|
||||||
String number,
|
|
||||||
Consumer<Manager> newManagerListener
|
|
||||||
) throws IOException {
|
|
||||||
final var accountPath = accountsStore.getPathByNumber(number);
|
|
||||||
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
|
||||||
final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
|
|
||||||
var aciIdentityKey = KeyUtils.generateIdentityKeyPair();
|
|
||||||
var pniIdentityKey = KeyUtils.generateIdentityKeyPair();
|
|
||||||
|
|
||||||
var profileKey = KeyUtils.createProfileKey();
|
|
||||||
var account = SignalAccount.create(pathConfig.dataPath(),
|
|
||||||
newAccountPath,
|
|
||||||
number,
|
|
||||||
serviceEnvironment,
|
|
||||||
aciIdentityKey,
|
|
||||||
pniIdentityKey,
|
|
||||||
profileKey,
|
|
||||||
settings);
|
|
||||||
account.initDatabase();
|
|
||||||
|
|
||||||
return new RegistrationManagerImpl(account,
|
|
||||||
pathConfig,
|
|
||||||
serviceEnvironmentConfig,
|
|
||||||
userAgent,
|
|
||||||
newManagerListener,
|
|
||||||
new AccountFileUpdaterImpl(accountsStore, newAccountPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
|
||||||
if (!number.equals(account.getNumber())) {
|
|
||||||
account.close();
|
|
||||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
|
||||||
}
|
|
||||||
account.initDatabase();
|
|
||||||
|
|
||||||
return new RegistrationManagerImpl(account,
|
|
||||||
pathConfig,
|
|
||||||
serviceEnvironmentConfig,
|
|
||||||
userAgent,
|
|
||||||
newManagerListener,
|
|
||||||
new AccountFileUpdaterImpl(accountsStore, accountPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public interface HandleAction {
|
|
||||||
|
|
||||||
void execute(Context context) throws Throwable;
|
|
||||||
|
|
||||||
default void mergeOther(HandleAction action) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class RefreshPreKeysAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final RefreshPreKeysAction INSTANCE = new RefreshPreKeysAction();
|
|
||||||
|
|
||||||
private RefreshPreKeysAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RefreshPreKeysAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getPreKeyHelper().refreshPreKeysIfNecessary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
import org.signal.core.models.ServiceId;
|
|
||||||
|
|
||||||
public class RenewSessionAction implements HandleAction {
|
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
|
||||||
private final ServiceId serviceId;
|
|
||||||
private final ServiceId accountId;
|
|
||||||
|
|
||||||
public RenewSessionAction(final RecipientId recipientId, final ServiceId serviceId, final ServiceId accountId) {
|
|
||||||
this.recipientId = recipientId;
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.accountId = accountId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
|
|
||||||
context.getSendHelper().sendNullMessage(recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
final RenewSessionAction that = (RenewSessionAction) o;
|
|
||||||
|
|
||||||
return recipientId.equals(that.recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return recipientId.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class ResendMessageAction implements HandleAction {
|
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
|
||||||
private final long timestamp;
|
|
||||||
private final MessageSendLogEntry messageSendLogEntry;
|
|
||||||
|
|
||||||
public ResendMessageAction(
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final long timestamp,
|
|
||||||
final MessageSendLogEntry messageSendLogEntry
|
|
||||||
) {
|
|
||||||
this.recipientId = recipientId;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.messageSendLogEntry = messageSendLogEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getSendHelper().resendMessage(recipientId, timestamp, messageSendLogEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
final ResendMessageAction that = (ResendMessageAction) o;
|
|
||||||
return timestamp == that.timestamp
|
|
||||||
&& recipientId.equals(that.recipientId)
|
|
||||||
&& messageSendLogEntry.equals(that.messageSendLogEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(recipientId, timestamp, messageSendLogEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class RetrieveDeviceNameAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final RetrieveDeviceNameAction INSTANCE = new RetrieveDeviceNameAction();
|
|
||||||
|
|
||||||
public static RetrieveDeviceNameAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RetrieveDeviceNameAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getAccountHelper().refreshDeviceName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
|
|
||||||
public class RetrieveProfileAction implements HandleAction {
|
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
|
||||||
|
|
||||||
public RetrieveProfileAction(final RecipientId recipientId) {
|
|
||||||
this.recipientId = recipientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getProfileHelper().refreshRecipientProfile(recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
final RetrieveProfileAction that = (RetrieveProfileAction) o;
|
|
||||||
|
|
||||||
return recipientId.equals(that.recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return recipientId.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.GroupIdV1;
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
|
|
||||||
public class SendGroupInfoAction implements HandleAction {
|
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
|
||||||
private final GroupIdV1 groupId;
|
|
||||||
|
|
||||||
public SendGroupInfoAction(final RecipientId recipientId, final GroupIdV1 groupId) {
|
|
||||||
this.recipientId = recipientId;
|
|
||||||
this.groupId = groupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getGroupHelper().sendGroupInfoMessage(groupId, recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
final var that = (SendGroupInfoAction) o;
|
|
||||||
|
|
||||||
if (!recipientId.equals(that.recipientId)) return false;
|
|
||||||
return groupId.equals(that.groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
var result = recipientId.hashCode();
|
|
||||||
result = 31 * result + groupId.hashCode();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.GroupIdV1;
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
|
|
||||||
public class SendGroupInfoRequestAction implements HandleAction {
|
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
|
||||||
private final GroupIdV1 groupId;
|
|
||||||
|
|
||||||
public SendGroupInfoRequestAction(final RecipientId recipientId, final GroupIdV1 groupId) {
|
|
||||||
this.recipientId = recipientId;
|
|
||||||
this.groupId = groupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getGroupHelper().sendGroupInfoRequest(groupId, recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
final var that = (SendGroupInfoRequestAction) o;
|
|
||||||
|
|
||||||
if (!recipientId.equals(that.recipientId)) return false;
|
|
||||||
return groupId.equals(that.groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
var result = recipientId.hashCode();
|
|
||||||
result = 31 * result + groupId.hashCode();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class SendProfileKeyAction implements HandleAction {
|
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
|
||||||
|
|
||||||
public SendProfileKeyAction(final RecipientId recipientId) {
|
|
||||||
this.recipientId = recipientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getSendHelper().sendProfileKey(recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
final SendProfileKeyAction that = (SendProfileKeyAction) o;
|
|
||||||
return recipientId.equals(that.recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(recipientId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class SendReceiptAction implements HandleAction {
|
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
|
||||||
private final SignalServiceReceiptMessage.Type type;
|
|
||||||
private final List<Long> timestamps = new ArrayList<>();
|
|
||||||
|
|
||||||
public SendReceiptAction(
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final SignalServiceReceiptMessage.Type type,
|
|
||||||
final long timestamp
|
|
||||||
) {
|
|
||||||
this.recipientId = recipientId;
|
|
||||||
this.type = type;
|
|
||||||
this.timestamps.add(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
final var receiptMessage = new SignalServiceReceiptMessage(type, timestamps, System.currentTimeMillis());
|
|
||||||
|
|
||||||
context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void mergeOther(final HandleAction action) {
|
|
||||||
if (action instanceof SendReceiptAction sendReceiptAction) {
|
|
||||||
this.timestamps.addAll(sendReceiptAction.timestamps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
final SendReceiptAction that = (SendReceiptAction) o;
|
|
||||||
// Using only recipientId and type here on purpose
|
|
||||||
return recipientId.equals(that.recipientId) && type == that.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
// Using only recipientId and type here on purpose
|
|
||||||
return Objects.hash(recipientId, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.GroupId;
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
import org.signal.libsignal.metadata.ProtocolException;
|
|
||||||
import org.signal.libsignal.protocol.message.CiphertextMessage;
|
|
||||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
|
||||||
import org.whispersystems.signalservice.internal.push.Envelope;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class SendRetryMessageRequestAction implements HandleAction {
|
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
|
||||||
private final ProtocolException protocolException;
|
|
||||||
private final SignalServiceEnvelope envelope;
|
|
||||||
|
|
||||||
public SendRetryMessageRequestAction(
|
|
||||||
final RecipientId recipientId,
|
|
||||||
final ProtocolException protocolException,
|
|
||||||
final SignalServiceEnvelope envelope
|
|
||||||
) {
|
|
||||||
this.recipientId = recipientId;
|
|
||||||
this.protocolException = protocolException;
|
|
||||||
this.envelope = envelope;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
int senderDevice = protocolException.getSenderDevice();
|
|
||||||
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
|
|
||||||
protocolException.getGroupId().get())) : Optional.empty();
|
|
||||||
|
|
||||||
byte[] originalContent;
|
|
||||||
int envelopeType;
|
|
||||||
if (protocolException.getUnidentifiedSenderMessageContent().isPresent()) {
|
|
||||||
final var messageContent = protocolException.getUnidentifiedSenderMessageContent().get();
|
|
||||||
originalContent = messageContent.getContent();
|
|
||||||
envelopeType = messageContent.getType();
|
|
||||||
} else {
|
|
||||||
originalContent = envelope.getContent();
|
|
||||||
envelopeType = envelope.getType() == null
|
|
||||||
? CiphertextMessage.WHISPER_TYPE
|
|
||||||
: envelopeTypeToCiphertextMessageType(envelope.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent,
|
|
||||||
envelopeType,
|
|
||||||
envelope.getTimestamp(),
|
|
||||||
senderDevice);
|
|
||||||
|
|
||||||
context.getSendHelper().sendRetryReceipt(decryptionErrorMessage, recipientId, groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int envelopeTypeToCiphertextMessageType(int envelopeType) {
|
|
||||||
final var type = Envelope.Type.fromValue(envelopeType);
|
|
||||||
if (type == null) {
|
|
||||||
return CiphertextMessage.WHISPER_TYPE;
|
|
||||||
}
|
|
||||||
return switch (type) {
|
|
||||||
case PREKEY_MESSAGE -> CiphertextMessage.PREKEY_TYPE;
|
|
||||||
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
|
|
||||||
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
|
|
||||||
default -> CiphertextMessage.WHISPER_TYPE;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
final SendRetryMessageRequestAction that = (SendRetryMessageRequestAction) o;
|
|
||||||
|
|
||||||
if (!recipientId.equals(that.recipientId)) return false;
|
|
||||||
if (!protocolException.equals(that.protocolException)) return false;
|
|
||||||
return envelope.equals(that.envelope);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = recipientId.hashCode();
|
|
||||||
result = 31 * result + protocolException.hashCode();
|
|
||||||
result = 31 * result + envelope.hashCode();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class SendSyncBlockedListAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction();
|
|
||||||
|
|
||||||
private SendSyncBlockedListAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SendSyncBlockedListAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getSyncHelper().sendBlockedList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class SendSyncConfigurationAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final SendSyncConfigurationAction INSTANCE = new SendSyncConfigurationAction();
|
|
||||||
|
|
||||||
private SendSyncConfigurationAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SendSyncConfigurationAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getSyncHelper().sendConfigurationMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class SendSyncContactsAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction();
|
|
||||||
|
|
||||||
private SendSyncContactsAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SendSyncContactsAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getSyncHelper().sendContacts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class SendSyncGroupsAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction();
|
|
||||||
|
|
||||||
private SendSyncGroupsAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SendSyncGroupsAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getSyncHelper().sendGroups();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class SendSyncKeysAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final SendSyncKeysAction INSTANCE = new SendSyncKeysAction();
|
|
||||||
|
|
||||||
private SendSyncKeysAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SendSyncKeysAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getSyncHelper().sendKeysMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
import org.asamk.signal.manager.jobs.SyncStorageJob;
|
|
||||||
|
|
||||||
public class SyncStorageDataAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final SyncStorageDataAction INSTANCE = new SyncStorageDataAction();
|
|
||||||
|
|
||||||
private SyncStorageDataAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SyncStorageDataAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class UpdateAccountAttributesAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final UpdateAccountAttributesAction INSTANCE = new UpdateAccountAttributesAction();
|
|
||||||
|
|
||||||
private UpdateAccountAttributesAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static UpdateAccountAttributesAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getAccountHelper().updateAccountAttributes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class AccountCheckException extends Exception {
|
|
||||||
|
|
||||||
public AccountCheckException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountCheckException(String message, Exception e) {
|
|
||||||
super(message, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class AlreadyReceivingException extends Exception {
|
|
||||||
|
|
||||||
public AlreadyReceivingException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AlreadyReceivingException(String message, Exception e) {
|
|
||||||
super(message, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public record CallInfo(
|
|
||||||
long callId,
|
|
||||||
State state,
|
|
||||||
RecipientAddress recipient,
|
|
||||||
String inputDeviceName,
|
|
||||||
String outputDeviceName,
|
|
||||||
boolean isOutgoing
|
|
||||||
) {
|
|
||||||
|
|
||||||
public enum State {
|
|
||||||
IDLE,
|
|
||||||
RINGING_INCOMING,
|
|
||||||
RINGING_OUTGOING,
|
|
||||||
CONNECTING,
|
|
||||||
CONNECTED,
|
|
||||||
RECONNECTING,
|
|
||||||
ENDED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public record CallOffer(
|
|
||||||
long callId, Type type, byte[] opaque
|
|
||||||
) {
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
AUDIO,
|
|
||||||
VIDEO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class CaptchaRejectedException extends Exception {
|
|
||||||
|
|
||||||
public CaptchaRejectedException() {
|
|
||||||
super("Captcha rejected");
|
|
||||||
}
|
|
||||||
|
|
||||||
public CaptchaRejectedException(final String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CaptchaRejectedException(final String message, final Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class CaptchaRequiredException extends Exception {
|
|
||||||
|
|
||||||
private long nextVerificationAttemptMilliseconds;
|
|
||||||
|
|
||||||
public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) {
|
|
||||||
super("Captcha required");
|
|
||||||
this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CaptchaRequiredException(final String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CaptchaRequiredException(final String message, final Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getNextVerificationAttemptMilliseconds() {
|
|
||||||
return nextVerificationAttemptMilliseconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public record Color(int color) {
|
|
||||||
|
|
||||||
public int alpha() {
|
|
||||||
return color >>> 24;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int red() {
|
|
||||||
return (color >> 16) & 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int green() {
|
|
||||||
return (color >> 8) & 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int blue() {
|
|
||||||
return color & 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toHexColor() {
|
|
||||||
return String.format("#%08x", color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public record Configuration(
|
|
||||||
Optional<Boolean> readReceipts,
|
|
||||||
Optional<Boolean> unidentifiedDeliveryIndicators,
|
|
||||||
Optional<Boolean> typingIndicators,
|
|
||||||
Optional<Boolean> linkPreviews
|
|
||||||
) {
|
|
||||||
|
|
||||||
public static Configuration from(final ConfigurationStore configurationStore) {
|
|
||||||
return new Configuration(Optional.ofNullable(configurationStore.getReadReceipts()),
|
|
||||||
Optional.ofNullable(configurationStore.getUnidentifiedDeliveryIndicators()),
|
|
||||||
Optional.ofNullable(configurationStore.getTypingIndicators()),
|
|
||||||
Optional.ofNullable(configurationStore.getLinkPreviews()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import org.whispersystems.signalservice.internal.util.Util;
|
|
||||||
|
|
||||||
public record Contact(
|
|
||||||
String givenName,
|
|
||||||
String familyName,
|
|
||||||
String nickName,
|
|
||||||
String nickNameGivenName,
|
|
||||||
String nickNameFamilyName,
|
|
||||||
String note,
|
|
||||||
String color,
|
|
||||||
int messageExpirationTime,
|
|
||||||
int messageExpirationTimeVersion,
|
|
||||||
long muteUntil,
|
|
||||||
boolean hideStory,
|
|
||||||
boolean isBlocked,
|
|
||||||
boolean isArchived,
|
|
||||||
boolean isProfileSharingEnabled,
|
|
||||||
boolean isHidden,
|
|
||||||
Long unregisteredTimestamp
|
|
||||||
) {
|
|
||||||
|
|
||||||
private Contact(final Builder builder) {
|
|
||||||
this(builder.givenName,
|
|
||||||
builder.familyName,
|
|
||||||
builder.nickName,
|
|
||||||
builder.nickNameGivenName,
|
|
||||||
builder.nickNameFamilyName,
|
|
||||||
builder.note,
|
|
||||||
builder.color,
|
|
||||||
builder.messageExpirationTime,
|
|
||||||
builder.messageExpirationTimeVersion,
|
|
||||||
builder.muteUntil,
|
|
||||||
builder.hideStory,
|
|
||||||
builder.isBlocked,
|
|
||||||
builder.isArchived,
|
|
||||||
builder.isProfileSharingEnabled,
|
|
||||||
builder.isHidden,
|
|
||||||
builder.unregisteredTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder newBuilder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder newBuilder(final Contact copy) {
|
|
||||||
Builder builder = new Builder();
|
|
||||||
builder.givenName = copy.givenName();
|
|
||||||
builder.familyName = copy.familyName();
|
|
||||||
builder.nickName = copy.nickName();
|
|
||||||
builder.nickNameGivenName = copy.nickNameGivenName();
|
|
||||||
builder.nickNameFamilyName = copy.nickNameFamilyName();
|
|
||||||
builder.note = copy.note();
|
|
||||||
builder.color = copy.color();
|
|
||||||
builder.messageExpirationTime = copy.messageExpirationTime();
|
|
||||||
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
|
|
||||||
builder.muteUntil = copy.muteUntil();
|
|
||||||
builder.hideStory = copy.hideStory();
|
|
||||||
builder.isBlocked = copy.isBlocked();
|
|
||||||
builder.isArchived = copy.isArchived();
|
|
||||||
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
|
||||||
builder.isHidden = copy.isHidden();
|
|
||||||
builder.unregisteredTimestamp = copy.unregisteredTimestamp();
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
final var noGivenName = Util.isEmpty(givenName);
|
|
||||||
final var noFamilyName = Util.isEmpty(familyName);
|
|
||||||
|
|
||||||
if (noGivenName && noFamilyName) {
|
|
||||||
return "";
|
|
||||||
} else if (noGivenName) {
|
|
||||||
return familyName;
|
|
||||||
} else if (noFamilyName) {
|
|
||||||
return givenName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return givenName + " " + familyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Builder {
|
|
||||||
|
|
||||||
private String givenName;
|
|
||||||
private String familyName;
|
|
||||||
private String nickName;
|
|
||||||
private String nickNameGivenName;
|
|
||||||
private String nickNameFamilyName;
|
|
||||||
private String note;
|
|
||||||
private String color;
|
|
||||||
private int messageExpirationTime;
|
|
||||||
private int messageExpirationTimeVersion = 1;
|
|
||||||
private long muteUntil;
|
|
||||||
private boolean hideStory;
|
|
||||||
private boolean isBlocked;
|
|
||||||
private boolean isArchived;
|
|
||||||
private boolean isProfileSharingEnabled;
|
|
||||||
private boolean isHidden;
|
|
||||||
private Long unregisteredTimestamp;
|
|
||||||
|
|
||||||
private Builder() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder newBuilder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withGivenName(final String val) {
|
|
||||||
givenName = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withFamilyName(final String val) {
|
|
||||||
familyName = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withNickName(final String val) {
|
|
||||||
nickName = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withNickNameGivenName(final String val) {
|
|
||||||
nickNameGivenName = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withNickNameFamilyName(final String val) {
|
|
||||||
nickNameFamilyName = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withNote(final String val) {
|
|
||||||
note = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withColor(final String val) {
|
|
||||||
color = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withMessageExpirationTime(final int val) {
|
|
||||||
messageExpirationTime = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withMessageExpirationTimeVersion(final int val) {
|
|
||||||
messageExpirationTimeVersion = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withMuteUntil(final long val) {
|
|
||||||
muteUntil = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withHideStory(final boolean val) {
|
|
||||||
hideStory = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withIsBlocked(final boolean val) {
|
|
||||||
isBlocked = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withIsArchived(final boolean val) {
|
|
||||||
isArchived = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withIsProfileSharingEnabled(final boolean val) {
|
|
||||||
isProfileSharingEnabled = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withIsHidden(final boolean val) {
|
|
||||||
isHidden = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withUnregisteredTimestamp(final Long val) {
|
|
||||||
unregisteredTimestamp = val;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Contact build() {
|
|
||||||
return new Contact(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public record Device(int id, String name, long created, long lastSeen, boolean isThisDevice) {}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class DeviceLimitExceededException extends Exception {
|
|
||||||
|
|
||||||
public DeviceLimitExceededException(final String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeviceLimitExceededException(final String message, final Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.util.Utils;
|
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
|
||||||
|
|
||||||
public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
|
|
||||||
|
|
||||||
public static DeviceLinkUrl parseDeviceLinkUri(URI linkUri) throws InvalidDeviceLinkException {
|
|
||||||
final var rawQuery = linkUri.getRawQuery();
|
|
||||||
if (isEmpty(rawQuery)) {
|
|
||||||
throw new RuntimeException("Invalid device link uri");
|
|
||||||
}
|
|
||||||
|
|
||||||
var query = Utils.getQueryMap(rawQuery);
|
|
||||||
var deviceIdentifier = query.get("uuid");
|
|
||||||
var publicKeyEncoded = query.get("pub_key");
|
|
||||||
|
|
||||||
if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) {
|
|
||||||
throw new InvalidDeviceLinkException("Invalid device link uri");
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] publicKeyBytes;
|
|
||||||
try {
|
|
||||||
publicKeyBytes = Base64.getDecoder().decode(publicKeyEncoded);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
throw new InvalidDeviceLinkException("Invalid device link uri", e);
|
|
||||||
}
|
|
||||||
ECPublicKey deviceKey;
|
|
||||||
try {
|
|
||||||
deviceKey = new ECPublicKey(publicKeyBytes);
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
throw new InvalidDeviceLinkException("Invalid device link", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DeviceLinkUrl(deviceIdentifier, deviceKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public URI createDeviceLinkUri() {
|
|
||||||
final var deviceKeyString = Base64.getEncoder().encodeToString(deviceKey.serialize()).replace("=", "");
|
|
||||||
try {
|
|
||||||
return new URI("sgnl://linkdevice?uuid="
|
|
||||||
+ URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8)
|
|
||||||
+ "&pub_key="
|
|
||||||
+ URLEncoder.encode(deviceKeyString, StandardCharsets.UTF_8));
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public record Group(
|
|
||||||
GroupId groupId,
|
|
||||||
String title,
|
|
||||||
String description,
|
|
||||||
GroupInviteLinkUrl groupInviteLinkUrl,
|
|
||||||
Set<GroupMember> members,
|
|
||||||
Set<RecipientAddress> pendingMembers,
|
|
||||||
Set<RecipientAddress> requestingMembers,
|
|
||||||
Set<RecipientAddress> bannedMembers,
|
|
||||||
boolean isBlocked,
|
|
||||||
int messageExpirationTimer,
|
|
||||||
GroupPermission permissionAddMember,
|
|
||||||
GroupPermission permissionEditDetails,
|
|
||||||
GroupPermission permissionSendMessage,
|
|
||||||
boolean isMember,
|
|
||||||
boolean isAdmin
|
|
||||||
) {
|
|
||||||
|
|
||||||
public static Group from(
|
|
||||||
final GroupInfo groupInfo,
|
|
||||||
final RecipientAddressResolver recipientStore,
|
|
||||||
final RecipientId selfRecipientId
|
|
||||||
) {
|
|
||||||
return new Group(groupInfo.getGroupId(),
|
|
||||||
groupInfo.getTitle(),
|
|
||||||
groupInfo.getDescription(),
|
|
||||||
groupInfo.getGroupInviteLink(),
|
|
||||||
groupInfo.getMembers()
|
|
||||||
.stream()
|
|
||||||
.map(m -> org.asamk.signal.manager.api.GroupMember.from(m, recipientStore))
|
|
||||||
.collect(Collectors.toSet()),
|
|
||||||
groupInfo.getPendingMembers()
|
|
||||||
.stream()
|
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
|
||||||
.collect(Collectors.toSet()),
|
|
||||||
groupInfo.getRequestingMembers()
|
|
||||||
.stream()
|
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
|
||||||
.collect(Collectors.toSet()),
|
|
||||||
groupInfo.getBannedMembers()
|
|
||||||
.stream()
|
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
|
||||||
.collect(Collectors.toSet()),
|
|
||||||
groupInfo.isBlocked(),
|
|
||||||
groupInfo.getMessageExpirationTimer(),
|
|
||||||
groupInfo.getPermissionAddMember(),
|
|
||||||
groupInfo.getPermissionEditDetails(),
|
|
||||||
groupInfo.getPermissionSendMessage(),
|
|
||||||
groupInfo.isMember(selfRecipientId),
|
|
||||||
groupInfo.isAdmin(selfRecipientId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
public abstract sealed class GroupId permits GroupIdV1, GroupIdV2 {
|
|
||||||
|
|
||||||
private final byte[] id;
|
|
||||||
|
|
||||||
public static GroupIdV1 v1(byte[] id) {
|
|
||||||
return new GroupIdV1(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GroupIdV2 v2(byte[] id) {
|
|
||||||
return new GroupIdV2(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GroupId unknownVersion(byte[] id) {
|
|
||||||
if (id.length == 16) {
|
|
||||||
return new GroupIdV1(id);
|
|
||||||
} else if (id.length == 32) {
|
|
||||||
return new GroupIdV2(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new AssertionError("Invalid group id of size " + id.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GroupId fromBase64(String id) throws GroupIdFormatException {
|
|
||||||
try {
|
|
||||||
return unknownVersion(java.util.Base64.getDecoder().decode(id));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new GroupIdFormatException(id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected GroupId(final byte[] id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] serialize() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toBase64() {
|
|
||||||
return Base64.getEncoder().encodeToString(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
final var groupId = (GroupId) o;
|
|
||||||
|
|
||||||
return Arrays.equals(id, groupId.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Arrays.hashCode(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class GroupIdFormatException extends Exception {
|
|
||||||
|
|
||||||
public GroupIdFormatException(String groupId, Throwable e) {
|
|
||||||
super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
|
|
||||||
|
|
||||||
public final class GroupIdV1 extends GroupId {
|
|
||||||
|
|
||||||
public static GroupIdV1 createRandom() {
|
|
||||||
return new GroupIdV1(getSecretBytes(16));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GroupIdV1 fromBase64(String groupId) {
|
|
||||||
return new GroupIdV1(Base64.getDecoder().decode(groupId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public GroupIdV1(final byte[] id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
public final class GroupIdV2 extends GroupId {
|
|
||||||
|
|
||||||
public static GroupIdV2 fromBase64(String groupId) {
|
|
||||||
return new GroupIdV2(Base64.getDecoder().decode(groupId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public GroupIdV2(final byte[] id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.groups.GroupLinkPassword;
|
|
||||||
import org.signal.core.util.Base64;
|
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
|
||||||
import org.signal.storageservice.storage.protos.groups.GroupInviteLink;
|
|
||||||
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
import okio.ByteString;
|
|
||||||
|
|
||||||
public final class GroupInviteLinkUrl {
|
|
||||||
|
|
||||||
private static final String GROUP_URL_HOST = "signal.group";
|
|
||||||
private static final String GROUP_URL_PREFIX = "https://" + GROUP_URL_HOST + "/#";
|
|
||||||
|
|
||||||
private final GroupMasterKey groupMasterKey;
|
|
||||||
private final GroupLinkPassword password;
|
|
||||||
private final String url;
|
|
||||||
|
|
||||||
public static GroupInviteLinkUrl forGroup(GroupMasterKey groupMasterKey, DecryptedGroup group) {
|
|
||||||
return new GroupInviteLinkUrl(groupMasterKey,
|
|
||||||
GroupLinkPassword.fromBytes(group.inviteLinkPassword.toByteArray()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return null iff not a group url.
|
|
||||||
* @throws InvalidGroupLinkException If group url, but cannot be parsed.
|
|
||||||
*/
|
|
||||||
public static GroupInviteLinkUrl fromUri(String urlString) throws InvalidGroupLinkException, UnknownGroupLinkVersionException {
|
|
||||||
var uri = getGroupUrl(urlString);
|
|
||||||
|
|
||||||
if (uri == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!"/".equals(uri.getPath()) && !uri.getPath().isEmpty()) {
|
|
||||||
throw new InvalidGroupLinkException("No path was expected in uri");
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoding = uri.getFragment();
|
|
||||||
|
|
||||||
if (encoding == null || encoding.isEmpty()) {
|
|
||||||
throw new InvalidGroupLinkException("No reference was in the uri");
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = Base64.decode(encoding);
|
|
||||||
GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes);
|
|
||||||
|
|
||||||
if (groupInviteLink.contentsV1 != null) {
|
|
||||||
var groupInviteLinkContentsV1 = groupInviteLink.contentsV1;
|
|
||||||
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray());
|
|
||||||
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray());
|
|
||||||
|
|
||||||
return new GroupInviteLinkUrl(groupMasterKey, password);
|
|
||||||
} else {
|
|
||||||
throw new UnknownGroupLinkVersionException("Url contains no known group link content");
|
|
||||||
}
|
|
||||||
} catch (InvalidInputException | IOException e) {
|
|
||||||
throw new InvalidGroupLinkException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {@link URI} if the host name matches.
|
|
||||||
*/
|
|
||||||
private static URI getGroupUrl(String urlString) {
|
|
||||||
try {
|
|
||||||
var url = new URI(urlString);
|
|
||||||
|
|
||||||
if (!"https".equalsIgnoreCase(url.getScheme()) && !"sgnl".equalsIgnoreCase(url.getScheme())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GROUP_URL_HOST.equalsIgnoreCase(url.getHost()) ? url : null;
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private GroupInviteLinkUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
|
||||||
this.groupMasterKey = groupMasterKey;
|
|
||||||
this.password = password;
|
|
||||||
this.url = createUrl(groupMasterKey, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
|
||||||
var groupInviteLink = new GroupInviteLink.Builder().contentsV1(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey(
|
|
||||||
ByteString.of(groupMasterKey.serialize()))
|
|
||||||
.inviteLinkPassword(ByteString.of(password.serialize()))
|
|
||||||
.build()).build();
|
|
||||||
|
|
||||||
var encoding = Base64.encodeUrlSafeWithoutPadding(groupInviteLink.encode());
|
|
||||||
|
|
||||||
return GROUP_URL_PREFIX + encoding;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GroupMasterKey getGroupMasterKey() {
|
|
||||||
return groupMasterKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GroupLinkPassword getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class InvalidGroupLinkException extends Exception {
|
|
||||||
|
|
||||||
public InvalidGroupLinkException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvalidGroupLinkException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class UnknownGroupLinkVersionException extends Exception {
|
|
||||||
|
|
||||||
public UnknownGroupLinkVersionException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public enum GroupLinkState {
|
|
||||||
ENABLED,
|
|
||||||
ENABLED_WITH_APPROVAL,
|
|
||||||
DISABLED,
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
|
||||||
import org.asamk.signal.manager.storage.groups.GroupMemberInfo;
|
|
||||||
|
|
||||||
public record GroupMember(
|
|
||||||
RecipientAddress recipientAddress, boolean isAdmin, String labelEmoji, String label
|
|
||||||
) {
|
|
||||||
|
|
||||||
public static GroupMember from(final GroupMemberInfo memberInfo, final RecipientAddressResolver recipientStore) {
|
|
||||||
return new GroupMember(recipientStore.resolveRecipientAddress(memberInfo.getRecipientId())
|
|
||||||
.toApiRecipientAddress(), memberInfo.isAdmin(), memberInfo.labelEmoji(), memberInfo.labelString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class GroupNotFoundException extends Exception {
|
|
||||||
|
|
||||||
public GroupNotFoundException(GroupId groupId) {
|
|
||||||
super("Group not found: " + groupId.toBase64());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public enum GroupPermission {
|
|
||||||
EVERY_MEMBER,
|
|
||||||
ONLY_ADMINS,
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class GroupSendingNotAllowedException extends Exception {
|
|
||||||
|
|
||||||
public GroupSendingNotAllowedException(GroupId groupId, String groupName) {
|
|
||||||
super("User is not allowed to send message to group: " + groupName + " (" + groupId.toBase64() + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public record Identity(
|
|
||||||
RecipientAddress recipient,
|
|
||||||
byte[] fingerprint,
|
|
||||||
String safetyNumber,
|
|
||||||
byte[] scannableSafetyNumber,
|
|
||||||
TrustLevel trustLevel,
|
|
||||||
long dateAddedTimestamp
|
|
||||||
) {}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import org.signal.libsignal.protocol.util.Hex;
|
|
||||||
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public sealed interface IdentityVerificationCode {
|
|
||||||
|
|
||||||
record Fingerprint(byte[] fingerprint) implements IdentityVerificationCode {}
|
|
||||||
|
|
||||||
record SafetyNumber(String safetyNumber) implements IdentityVerificationCode {}
|
|
||||||
|
|
||||||
record ScannableSafetyNumber(byte[] safetyNumber) implements IdentityVerificationCode {}
|
|
||||||
|
|
||||||
static IdentityVerificationCode parse(String code) throws Exception {
|
|
||||||
code = code.replaceAll(" ", "");
|
|
||||||
if (code.length() == 66) {
|
|
||||||
final var fingerprintBytes = Hex.fromStringCondensed(code.toLowerCase(Locale.ROOT));
|
|
||||||
return new Fingerprint(fingerprintBytes);
|
|
||||||
} else if (code.length() == 60) {
|
|
||||||
return new SafetyNumber(code);
|
|
||||||
} else {
|
|
||||||
final var scannableSafetyNumber = Base64.getDecoder().decode(code);
|
|
||||||
return new ScannableSafetyNumber(scannableSafetyNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class InactiveGroupLinkException extends Exception {
|
|
||||||
|
|
||||||
public InactiveGroupLinkException(final String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InactiveGroupLinkException(final String message, final Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class IncorrectPinException extends Exception {
|
|
||||||
|
|
||||||
private final int triesRemaining;
|
|
||||||
|
|
||||||
public IncorrectPinException(int triesRemaining) {
|
|
||||||
this.triesRemaining = triesRemaining;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTriesRemaining() {
|
|
||||||
return triesRemaining;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class InvalidDeviceLinkException extends Exception {
|
|
||||||
|
|
||||||
public InvalidDeviceLinkException(final String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvalidDeviceLinkException(final String message, final Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class InvalidNumberException extends Exception {
|
|
||||||
|
|
||||||
public InvalidNumberException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
InvalidNumberException(String message, Throwable e) {
|
|
||||||
super(message, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class InvalidStickerException extends Exception {
|
|
||||||
|
|
||||||
public InvalidStickerException(final String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvalidStickerException(final String message, final Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class InvalidUsernameException extends Exception {
|
|
||||||
|
|
||||||
public InvalidUsernameException(final String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvalidUsernameException(final String message, final Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
public class LastGroupAdminException extends Exception {
|
|
||||||
|
|
||||||
public LastGroupAdminException(GroupId groupId, String groupName) {
|
|
||||||
super("User is last admin in group: " + groupName + " (" + groupId.toBase64() + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package org.asamk.signal.manager.api;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public record Message(
|
|
||||||
String messageText,
|
|
||||||
List<String> attachments,
|
|
||||||
boolean viewOnce,
|
|
||||||
boolean voiceNote,
|
|
||||||
List<Mention> mentions,
|
|
||||||
Optional<Quote> quote,
|
|
||||||
Optional<Sticker> sticker,
|
|
||||||
List<Preview> previews,
|
|
||||||
Optional<StoryReply> storyReply,
|
|
||||||
List<TextStyle> textStyles,
|
|
||||||
boolean urgent
|
|
||||||
) {
|
|
||||||
|
|
||||||
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
|
||||||
|
|
||||||
public record Quote(
|
|
||||||
long timestamp,
|
|
||||||
RecipientIdentifier.Single author,
|
|
||||||
String message,
|
|
||||||
List<Mention> mentions,
|
|
||||||
List<TextStyle> textStyles,
|
|
||||||
List<Attachment> attachments
|
|
||||||
) {
|
|
||||||
|
|
||||||
public record Attachment(String contentType, String filename, String preview) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record Sticker(byte[] packId, int stickerId) {}
|
|
||||||
|
|
||||||
public record Preview(String url, String title, String description, Optional<String> image) {}
|
|
||||||
|
|
||||||
public record StoryReply(long timestamp, RecipientIdentifier.Single author) {}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user