mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-05-19 13:24:16 +00:00
Compare commits
6 Commits
87a1cf6bab
...
daef4d1cf8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daef4d1cf8 | ||
|
|
b498d2050a | ||
|
|
27a722dc75 | ||
|
|
7014f629fe | ||
|
|
30b57bdb3d | ||
|
|
b94162afbc |
48
.github/workflows/build.yml
vendored
Normal file
48
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
workflow_call:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Build
|
||||
run: ./reproducible-builds/build.sh
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signal-cli-build
|
||||
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
|
||||
96
.github/workflows/ci.yml
vendored
96
.github/workflows/ci.yml
vendored
@ -1,96 +0,0 @@
|
||||
name: signal-cli CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: write # to fetch code (actions/checkout) and submit dependency graph (gradle/gradle-build-action)
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ '25' ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: ${{ matrix.java }}
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
dependency-graph: generate-and-submit
|
||||
- name: Install asciidoc
|
||||
run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew --no-daemon build
|
||||
- name: Build man page
|
||||
run: |
|
||||
cd man
|
||||
make install
|
||||
- name: Add man page to archive
|
||||
run: |
|
||||
version=$(tar tf build/distributions/signal-cli-*.tar | head -n1 | sed 's|signal-cli-\([^/]*\)/.*|\1|')
|
||||
echo $version
|
||||
tar --transform="flags=r;s|man|signal-cli-${version}/man|" -rf build/distributions/signal-cli-${version}.tar man/man{1,5}
|
||||
- name: Compress archive
|
||||
run: gzip -n -9 build/distributions/signal-cli-*.tar
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signal-cli-archive-${{ matrix.java }}
|
||||
path: build/distributions/signal-cli-*.tar.gz
|
||||
|
||||
build-graalvm:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
distribution: 'graalvm'
|
||||
java-version: '25'
|
||||
cache: 'gradle'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew --no-daemon nativeCompile
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signal-cli-native
|
||||
path: build/native/nativeCompile/signal-cli
|
||||
|
||||
build-client:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu
|
||||
- macos
|
||||
- windows
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./client
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install rust
|
||||
run: rustup default stable
|
||||
- name: Build client
|
||||
run: cargo build --release --verbose
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signal-cli-client-${{ matrix.os }}
|
||||
path: |
|
||||
client/target/release/signal-cli-client
|
||||
client/target/release/signal-cli-client.exe
|
||||
200
.github/workflows/release.yml
vendored
200
.github/workflows/release.yml
vendored
@ -5,8 +5,7 @@ on:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
permissions:
|
||||
contents: write # to fetch code (actions/checkout) and create release
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
IMAGE_NAME: signal-cli
|
||||
@ -15,96 +14,25 @@ env:
|
||||
REGISTRY_PASSWORD: ${{ github.token }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: AsamK/signal-cli/.github/workflows/build.yml@master
|
||||
|
||||
ci_wf:
|
||||
permissions:
|
||||
contents: write
|
||||
uses: AsamK/signal-cli/.github/workflows/ci.yml@master
|
||||
# ${{ github.repository }} not accepted here
|
||||
|
||||
lib_to_jar:
|
||||
needs: ci_wf
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
outputs:
|
||||
signal_cli_version: ${{ steps.cli_ver.outputs.version }}
|
||||
release_id: ${{ steps.create_release.outputs.id }}
|
||||
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
|
||||
- name: Download signal-cli build from CI workflow
|
||||
uses: actions/download-artifact@v8
|
||||
|
||||
- name: Get signal-cli version
|
||||
id: cli_ver
|
||||
id: version
|
||||
run: |
|
||||
ver="${GITHUB_REF_NAME#v}"
|
||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract archive
|
||||
run: |
|
||||
tree .
|
||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
||||
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
|
||||
rm -rf signal-cli-archive-*/
|
||||
|
||||
# - name: Get signal-client jar version
|
||||
# id: lib_ver
|
||||
# run: |
|
||||
# JAR_PREFIX=libsignal-client-
|
||||
# jar_file=$(find ./signal-cli-*/lib/ -name "$JAR_PREFIX*.jar")
|
||||
# jar_version=$(echo "$jar_file" | xargs basename | sed "s/$JAR_PREFIX//; s/.jar//")
|
||||
# echo "$jar_version"
|
||||
# echo "signal_client_version=${jar_version}" >> $GITHUB_OUTPUT
|
||||
#
|
||||
# - name: Download signal-client builds
|
||||
# env:
|
||||
# RELEASES_URL: https://github.com/signalapp/libsignal/releases/download/
|
||||
# FILE_NAMES: signal_jni.dll libsignal_jni.dylib
|
||||
# SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
|
||||
# run: |
|
||||
# for file_name in $FILE_NAMES; do
|
||||
# curl -sOL "${RELEASES_URL}/v${SIGNAL_CLIENT_VER}/${file_name}" # note: added v
|
||||
# done
|
||||
# tree .
|
||||
|
||||
- name: Compress native app
|
||||
env:
|
||||
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
||||
run: |
|
||||
chmod +x signal-cli-native/signal-cli
|
||||
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-native.tar.gz -C signal-cli-native signal-cli
|
||||
rm -rf signal-cli-native/
|
||||
|
||||
- name: Compress client app
|
||||
env:
|
||||
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
||||
run: |
|
||||
chmod +x signal-cli-client-ubuntu/signal-cli-client
|
||||
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-client.tar.gz -C signal-cli-client-ubuntu signal-cli-client
|
||||
rm -rf signal-cli-client-ubuntu/
|
||||
|
||||
# - name: Replace Windows lib
|
||||
# env:
|
||||
# SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
||||
# SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
|
||||
# run: |
|
||||
# mv signal_jni.dll libsignal_jni.so
|
||||
# zip -u ./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar ./libsignal_jni.so
|
||||
# tar -czf signal-cli-${SIGNAL_CLI_VER}-Windows.tar.gz signal-cli-*/
|
||||
#
|
||||
# - name: Replace macOS lib
|
||||
# env:
|
||||
# SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
||||
# SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
|
||||
# run: |
|
||||
# jar_file=./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar
|
||||
# zip -d $jar_file libsignal_jni.so
|
||||
# zip $jar_file libsignal_jni.dylib
|
||||
# tar -czf signal-cli-${SIGNAL_CLI_VER}-macOS.tar.gz signal-cli-*/
|
||||
mv ./signal-cli-build/* .
|
||||
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
@ -112,8 +40,8 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
|
||||
release_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
|
||||
tag_name: v${{ steps.version.outputs.version }} # note: added `v`
|
||||
release_name: v${{ steps.version.outputs.version }} # note: added `v`
|
||||
draft: true
|
||||
|
||||
- name: Upload archive
|
||||
@ -122,19 +50,9 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
|
||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
|
||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
|
||||
# - name: Upload Linux archive
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
|
||||
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
|
||||
# asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
asset_path: signal-cli-${{ steps.version.outputs.version }}.tar.gz
|
||||
asset_name: signal-cli-${{ steps.version.outputs.version }}.tar.gz
|
||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
|
||||
- name: Upload Linux native archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
@ -142,9 +60,9 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
|
||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
|
||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
asset_path: signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz
|
||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz
|
||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
|
||||
- name: Upload Linux client archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
@ -152,35 +70,14 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-client.tar.gz
|
||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-client.tar.gz
|
||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
|
||||
# - name: Upload windows archive
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
|
||||
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
|
||||
# asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
#
|
||||
# - name: Upload macos archive
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
|
||||
# asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
|
||||
# asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
asset_path: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
|
||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
|
||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
|
||||
build-container:
|
||||
needs: ci_wf
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
@ -188,28 +85,19 @@ jobs:
|
||||
- name: Download signal-cli build from CI workflow
|
||||
uses: actions/download-artifact@v8
|
||||
|
||||
- name: Get signal-cli version
|
||||
id: cli_ver
|
||||
run: |
|
||||
ver="${GITHUB_REF_NAME#v}"
|
||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Move archive file
|
||||
run: |
|
||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
||||
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||
rm -r signal-cli-archive-* signal-cli-native
|
||||
tar xf ./signal-cli-build/signal-cli-${{ needs.release.outputs.version }}.tar.gz
|
||||
mkdir -p build/install/
|
||||
mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
|
||||
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: ${{ env.IMAGE_NAME }}
|
||||
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
|
||||
containerfiles:
|
||||
./Containerfile
|
||||
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
||||
containerfiles: ./Containerfile
|
||||
oci: true
|
||||
|
||||
- name: Push To GHCR
|
||||
@ -227,10 +115,9 @@ jobs:
|
||||
echo "${{ toJSON(steps.push.outputs) }}"
|
||||
|
||||
build-container-native:
|
||||
needs: ci_wf
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
@ -238,26 +125,20 @@ jobs:
|
||||
- name: Download signal-cli build from CI workflow
|
||||
uses: actions/download-artifact@v8
|
||||
|
||||
- name: Get signal-cli version
|
||||
id: cli_ver
|
||||
run: |
|
||||
ver="${GITHUB_REF_NAME#v}"
|
||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Move archive file
|
||||
run: |
|
||||
tar xf ./signal-cli-build/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
|
||||
mkdir -p build/native/nativeCompile/
|
||||
chmod +x ./signal-cli-native/signal-cli
|
||||
mv ./signal-cli-native/signal-cli build/native/nativeCompile/
|
||||
mv signal-cli build/native/nativeCompile/
|
||||
chmod +x build/native/nativeCompile/signal-cli
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: ${{ env.IMAGE_NAME }}
|
||||
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
|
||||
containerfiles:
|
||||
./native.Containerfile
|
||||
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
||||
containerfiles: ./native.Containerfile
|
||||
oci: true
|
||||
|
||||
- name: Push To GHCR
|
||||
@ -275,10 +156,9 @@ jobs:
|
||||
echo "${{ toJSON(steps.push.outputs) }}"
|
||||
|
||||
build-container-client:
|
||||
needs: ci_wf
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
@ -286,26 +166,20 @@ jobs:
|
||||
- name: Download signal-cli build from CI workflow
|
||||
uses: actions/download-artifact@v8
|
||||
|
||||
- name: Get signal-cli version
|
||||
id: cli_ver
|
||||
run: |
|
||||
ver="${GITHUB_REF_NAME#v}"
|
||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Move archive file
|
||||
run: |
|
||||
tar xf ./signal-cli-build/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
|
||||
mkdir -p client/target/release/
|
||||
chmod +x ./signal-cli-client-ubuntu/signal-cli-client
|
||||
mv ./signal-cli-client-ubuntu/signal-cli-client client/target/release/
|
||||
mv signal-cli-client client/target/release/
|
||||
chmod +x client/target/release/signal-cli-client
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: ${{ env.IMAGE_NAME }}
|
||||
tags: latest-client ${{ github.sha }}-client ${{ steps.cli_ver.outputs.version }}-client
|
||||
containerfiles:
|
||||
./client.Containerfile
|
||||
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
||||
containerfiles: ./client.Containerfile
|
||||
oci: true
|
||||
|
||||
- name: Push To GHCR
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.gradle/
|
||||
.kotlin/
|
||||
.idea/*
|
||||
!.idea/codeStyles/
|
||||
build/
|
||||
@ -13,3 +14,9 @@ out/
|
||||
.DS_Store
|
||||
/bin/
|
||||
/test-config/
|
||||
/dist/
|
||||
/github/
|
||||
man/*.1
|
||||
man/*.5
|
||||
man/man1
|
||||
man/man5
|
||||
|
||||
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,6 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
## [0.14.1] - 2026-03-08
|
||||
|
||||
### Added
|
||||
|
||||
- Added isArchived to contact json output (Thanks @moppman)
|
||||
- Added support for group member labels
|
||||
|
||||
### Fixed
|
||||
|
||||
- Adapt registration to signal server changes
|
||||
|
||||
## [0.14.0] - 2026-03-01
|
||||
|
||||
|
||||
@ -3,12 +3,12 @@ plugins {
|
||||
application
|
||||
eclipse
|
||||
`check-lib-versions`
|
||||
id("org.graalvm.buildtools.native") version "0.11.4"
|
||||
id("org.graalvm.buildtools.native") version "0.11.5"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "org.asamk"
|
||||
version = "0.14.1-SNAPSHOT"
|
||||
version = "0.14.1"
|
||||
}
|
||||
|
||||
java {
|
||||
|
||||
@ -598,6 +598,12 @@ pub enum CliCommands {
|
||||
|
||||
#[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")]
|
||||
|
||||
@ -460,6 +460,8 @@ pub trait Rpc {
|
||||
#[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)]
|
||||
|
||||
@ -535,6 +535,8 @@ async fn handle_command(
|
||||
set_permission_edit_details,
|
||||
set_permission_send_messages,
|
||||
expiration,
|
||||
member_label_emoji,
|
||||
member_label,
|
||||
} => {
|
||||
client
|
||||
.update_group(
|
||||
@ -568,6 +570,8 @@ async fn handle_command(
|
||||
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
||||
}),
|
||||
expiration,
|
||||
member_label_emoji,
|
||||
member_label,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -45,6 +45,9 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<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>
|
||||
|
||||
@ -12,10 +12,9 @@ public record Group(
|
||||
String title,
|
||||
String description,
|
||||
GroupInviteLinkUrl groupInviteLinkUrl,
|
||||
Set<RecipientAddress> members,
|
||||
Set<GroupMember> members,
|
||||
Set<RecipientAddress> pendingMembers,
|
||||
Set<RecipientAddress> requestingMembers,
|
||||
Set<RecipientAddress> adminMembers,
|
||||
Set<RecipientAddress> bannedMembers,
|
||||
boolean isBlocked,
|
||||
int messageExpirationTimer,
|
||||
@ -37,8 +36,7 @@ public record Group(
|
||||
groupInfo.getGroupInviteLink(),
|
||||
groupInfo.getMembers()
|
||||
.stream()
|
||||
.map(recipientStore::resolveRecipientAddress)
|
||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
||||
.map(m -> org.asamk.signal.manager.api.GroupMember.from(m, recipientStore))
|
||||
.collect(Collectors.toSet()),
|
||||
groupInfo.getPendingMembers()
|
||||
.stream()
|
||||
@ -50,11 +48,6 @@ public record Group(
|
||||
.map(recipientStore::resolveRecipientAddress)
|
||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
||||
.collect(Collectors.toSet()),
|
||||
groupInfo.getAdminMembers()
|
||||
.stream()
|
||||
.map(recipientStore::resolveRecipientAddress)
|
||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
||||
.collect(Collectors.toSet()),
|
||||
groupInfo.getBannedMembers()
|
||||
.stream()
|
||||
.map(recipientStore::resolveRecipientAddress)
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,8 @@ public class UpdateGroup {
|
||||
private final String avatarFile;
|
||||
private final Integer expirationTimer;
|
||||
private final Boolean isAnnouncementGroup;
|
||||
private final String labelEmoji;
|
||||
private final String labelString;
|
||||
|
||||
private UpdateGroup(final Builder builder) {
|
||||
name = builder.name;
|
||||
@ -36,6 +38,8 @@ public class UpdateGroup {
|
||||
avatarFile = builder.avatarFile;
|
||||
expirationTimer = builder.expirationTimer;
|
||||
isAnnouncementGroup = builder.isAnnouncementGroup;
|
||||
labelEmoji = builder.labelEmoji;
|
||||
labelString = builder.labelString;
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
@ -57,7 +61,9 @@ public class UpdateGroup {
|
||||
copy.editDetailsPermission,
|
||||
copy.avatarFile,
|
||||
copy.expirationTimer,
|
||||
copy.isAnnouncementGroup);
|
||||
copy.isAnnouncementGroup,
|
||||
copy.labelEmoji,
|
||||
copy.labelString);
|
||||
}
|
||||
|
||||
public static Builder newBuilder(
|
||||
@ -75,7 +81,9 @@ public class UpdateGroup {
|
||||
final GroupPermission editDetailsPermission,
|
||||
final String avatarFile,
|
||||
final Integer expirationTimer,
|
||||
final Boolean isAnnouncementGroup
|
||||
final Boolean isAnnouncementGroup,
|
||||
final String labelEmoji,
|
||||
final String labelString
|
||||
) {
|
||||
return new Builder(name,
|
||||
description,
|
||||
@ -91,7 +99,9 @@ public class UpdateGroup {
|
||||
editDetailsPermission,
|
||||
avatarFile,
|
||||
expirationTimer,
|
||||
isAnnouncementGroup);
|
||||
isAnnouncementGroup,
|
||||
labelEmoji,
|
||||
labelString);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@ -154,6 +164,14 @@ public class UpdateGroup {
|
||||
return isAnnouncementGroup;
|
||||
}
|
||||
|
||||
public String getLabelEmoji() {
|
||||
return labelEmoji;
|
||||
}
|
||||
|
||||
public String getLabelString() {
|
||||
return labelString;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String name;
|
||||
@ -171,6 +189,8 @@ public class UpdateGroup {
|
||||
private String avatarFile;
|
||||
private Integer expirationTimer;
|
||||
private Boolean isAnnouncementGroup;
|
||||
private String labelEmoji;
|
||||
private String labelString;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
@ -190,7 +210,9 @@ public class UpdateGroup {
|
||||
final GroupPermission editDetailsPermission,
|
||||
final String avatarFile,
|
||||
final Integer expirationTimer,
|
||||
final Boolean isAnnouncementGroup
|
||||
final Boolean isAnnouncementGroup,
|
||||
final String labelEmoji,
|
||||
final String labelString
|
||||
) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
@ -207,6 +229,8 @@ public class UpdateGroup {
|
||||
this.avatarFile = avatarFile;
|
||||
this.expirationTimer = expirationTimer;
|
||||
this.isAnnouncementGroup = isAnnouncementGroup;
|
||||
this.labelEmoji = labelEmoji;
|
||||
this.labelString = labelString;
|
||||
}
|
||||
|
||||
public Builder withName(final String val) {
|
||||
@ -284,6 +308,16 @@ public class UpdateGroup {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withLabelEmoji(final String val) {
|
||||
labelEmoji = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withLabelString(final String val) {
|
||||
labelString = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UpdateGroup build() {
|
||||
return new UpdateGroup(this);
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public class ServiceConfig {
|
||||
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
||||
final var attachmentBackfill = !isPrimaryDevice;
|
||||
final var spqr = !isPrimaryDevice;
|
||||
final var spqr = true;
|
||||
return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr);
|
||||
}
|
||||
|
||||
|
||||
@ -299,7 +299,9 @@ public class GroupHelper {
|
||||
final GroupPermission editDetailsPermission,
|
||||
final String avatarFile,
|
||||
final Integer expirationTimer,
|
||||
final Boolean isAnnouncementGroup
|
||||
final Boolean isAnnouncementGroup,
|
||||
final String labelEmoji,
|
||||
final String labelString
|
||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
||||
var group = getGroupForUpdating(groupId);
|
||||
final var avatarBytes = readAvatarBytes(avatarFile);
|
||||
@ -323,7 +325,9 @@ public class GroupHelper {
|
||||
editDetailsPermission,
|
||||
avatarBytes,
|
||||
expirationTimer,
|
||||
isAnnouncementGroup);
|
||||
isAnnouncementGroup,
|
||||
labelEmoji,
|
||||
labelString);
|
||||
} catch (ConflictException e) {
|
||||
// Detected conflicting update, refreshing group and trying again
|
||||
group = getGroup(groupId, true);
|
||||
@ -342,7 +346,9 @@ public class GroupHelper {
|
||||
editDetailsPermission,
|
||||
avatarBytes,
|
||||
expirationTimer,
|
||||
isAnnouncementGroup);
|
||||
isAnnouncementGroup,
|
||||
labelEmoji,
|
||||
labelString);
|
||||
}
|
||||
}
|
||||
|
||||
@ -701,7 +707,9 @@ public class GroupHelper {
|
||||
final GroupPermission editDetailsPermission,
|
||||
final byte[] avatarFile,
|
||||
final Integer expirationTimer,
|
||||
final Boolean isAnnouncementGroup
|
||||
final Boolean isAnnouncementGroup,
|
||||
final String labelEmoji,
|
||||
final String labelString
|
||||
) throws IOException {
|
||||
SendGroupMessageResults result = null;
|
||||
final var groupV2Helper = context.getGroupV2Helper();
|
||||
@ -758,7 +766,7 @@ public class GroupHelper {
|
||||
if (admins != null) {
|
||||
final var newAdmins = new HashSet<>(admins);
|
||||
newAdmins.retainAll(group.getMembers());
|
||||
newAdmins.removeAll(group.getAdminMembers());
|
||||
newAdmins.removeAll(group.getAdminMemberRecipientIds());
|
||||
if (!newAdmins.isEmpty()) {
|
||||
for (var admin : newAdmins) {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
|
||||
@ -771,7 +779,7 @@ public class GroupHelper {
|
||||
|
||||
if (removeAdmins != null) {
|
||||
final var existingRemoveAdmins = new HashSet<>(removeAdmins);
|
||||
existingRemoveAdmins.retainAll(group.getAdminMembers());
|
||||
existingRemoveAdmins.retainAll(group.getAdminMemberRecipientIds());
|
||||
if (!existingRemoveAdmins.isEmpty()) {
|
||||
for (var admin : existingRemoveAdmins) {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
|
||||
@ -830,6 +838,15 @@ public class GroupHelper {
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (labelString != null || labelEmoji != null) {
|
||||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
final var selfMember = group.getMember(selfRecipientId);
|
||||
var groupGroupChangePair = groupV2Helper.setMemberLabels(group,
|
||||
labelEmoji != null ? labelEmoji : selfMember.labelEmoji(),
|
||||
labelString != null ? labelString : selfMember.labelString());
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (name != null || description != null || avatarFile != null) {
|
||||
var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
|
||||
if (avatarFile != null) {
|
||||
@ -859,7 +876,7 @@ public class GroupHelper {
|
||||
final GroupInfoV2 groupInfoV2,
|
||||
final Set<RecipientId> newAdmins
|
||||
) throws LastGroupAdminException, IOException {
|
||||
final var currentAdmins = groupInfoV2.getAdminMembers();
|
||||
final var currentAdmins = groupInfoV2.getAdminMemberRecipientIds();
|
||||
newAdmins.removeAll(currentAdmins);
|
||||
newAdmins.retainAll(groupInfoV2.getMembers());
|
||||
if (currentAdmins.contains(account.getSelfRecipientId())
|
||||
@ -888,7 +905,7 @@ public class GroupHelper {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
|
||||
.withId(g.getGroupId().serialize())
|
||||
.withName(g.name)
|
||||
.withMembers(g.getMembers()
|
||||
.withMembers(g.getMemberRecipientIds()
|
||||
.stream()
|
||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||
.toList());
|
||||
|
||||
@ -533,6 +533,18 @@ class GroupV2Helper {
|
||||
return commitChange(groupInfoV2, change);
|
||||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setMemberLabels(
|
||||
GroupInfoV2 groupInfoV2,
|
||||
String labelEmoji,
|
||||
String labelString
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
final var change = groupOperations.createChangeMemberLabel(getSelfAci(),
|
||||
labelString == null ? "" : labelString,
|
||||
labelEmoji);
|
||||
return commitChange(groupInfoV2, change);
|
||||
}
|
||||
|
||||
private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
|
||||
return switch (state) {
|
||||
case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE;
|
||||
|
||||
@ -320,7 +320,9 @@ public class SendHelper {
|
||||
messageBuilder.withExpiration(g.getMessageExpirationTimer());
|
||||
|
||||
final var message = messageBuilder.build();
|
||||
final var recipients = includeSelf ? g.getMembers() : g.getMembersWithout(account.getSelfRecipientId());
|
||||
final var recipients = includeSelf
|
||||
? g.getMemberRecipientIds()
|
||||
: g.getMembersWithout(account.getSelfRecipientId());
|
||||
|
||||
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
|
||||
if (message.getBody().isPresent()
|
||||
|
||||
@ -114,7 +114,7 @@ public class SyncHelper {
|
||||
if (record instanceof GroupInfoV1 groupInfo) {
|
||||
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
|
||||
Optional.ofNullable(groupInfo.name),
|
||||
groupInfo.getMembers()
|
||||
groupInfo.getMemberRecipientIds()
|
||||
.stream()
|
||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||
.toList(),
|
||||
|
||||
@ -633,7 +633,9 @@ public class ManagerImpl implements Manager {
|
||||
updateGroup.getEditDetailsPermission(),
|
||||
updateGroup.getAvatarFile(),
|
||||
updateGroup.getExpirationTimer(),
|
||||
updateGroup.getIsAnnouncementGroup());
|
||||
updateGroup.getIsAnnouncementGroup(),
|
||||
updateGroup.getLabelEmoji(),
|
||||
updateGroup.getLabelString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -6,6 +6,7 @@ import org.asamk.signal.manager.api.GroupPermission;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@ -24,7 +25,18 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
||||
|
||||
public abstract GroupInviteLinkUrl getGroupInviteLink();
|
||||
|
||||
public abstract Set<RecipientId> getMembers();
|
||||
public abstract Collection<GroupMemberInfo> getMembers();
|
||||
|
||||
public Set<RecipientId> getMemberRecipientIds() {
|
||||
return getMembers().stream().map(GroupMemberInfo::getRecipientId).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public GroupMemberInfo getMember(RecipientId recipientId) {
|
||||
return getMembers().stream()
|
||||
.filter(member -> member.getRecipientId().equals(recipientId))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
public Set<RecipientId> getBannedMembers() {
|
||||
return Set.of();
|
||||
@ -38,7 +50,7 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
public Set<RecipientId> getAdminMembers() {
|
||||
public Set<RecipientId> getAdminMemberRecipientIds() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@ -61,21 +73,23 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
||||
public abstract GroupPermission getPermissionSendMessage();
|
||||
|
||||
public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
|
||||
return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
|
||||
return getMemberRecipientIds().stream()
|
||||
.filter(member -> !member.equals(recipientId))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public Set<RecipientId> getMembersIncludingPendingWithout(RecipientId recipientId) {
|
||||
return Stream.concat(getMembers().stream(), getPendingMembers().stream())
|
||||
return Stream.concat(getMemberRecipientIds().stream(), getPendingMembers().stream())
|
||||
.filter(member -> !member.equals(recipientId))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public boolean isMember(RecipientId recipientId) {
|
||||
return getMembers().contains(recipientId);
|
||||
return getMembers().stream().anyMatch(m -> m.getRecipientId().equals(recipientId));
|
||||
}
|
||||
|
||||
public boolean isAdmin(RecipientId recipientId) {
|
||||
return getAdminMembers().contains(recipientId);
|
||||
return getMembers().stream().anyMatch(m -> m.isAdmin() && m.getRecipientId().equals(recipientId));
|
||||
}
|
||||
|
||||
public boolean isPendingMember(RecipientId recipientId) {
|
||||
|
||||
@ -80,8 +80,8 @@ public final class GroupInfoV1 extends GroupInfo {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Set<RecipientId> getMembers() {
|
||||
return new HashSet<>(members);
|
||||
public Collection<GroupMemberInfo> getMembers() {
|
||||
return members.stream().map(m -> (GroupMemberInfo) new GroupMemberInfoV1(m)).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -3,18 +3,17 @@ package org.asamk.signal.manager.storage.groups;
|
||||
import org.asamk.signal.manager.api.GroupIdV2;
|
||||
import org.asamk.signal.manager.api.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.api.GroupPermission;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.signal.core.models.ServiceId;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.storageservice.storage.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.storage.protos.groups.Member;
|
||||
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.storage.protos.groups.local.EnabledState;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -122,14 +121,11 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RecipientId> getMembers() {
|
||||
public Collection<GroupMemberInfo> getMembers() {
|
||||
if (this.group == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return group.members.stream()
|
||||
.map(m -> ServiceId.parseOrThrow(m.aciBytes))
|
||||
.map(recipientResolver::resolveRecipient)
|
||||
.collect(Collectors.toSet());
|
||||
return group.members.stream().map(m -> (GroupMemberInfo) new GroupMemberInfoV2(m, recipientResolver)).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -175,16 +171,11 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RecipientId> getAdminMembers() {
|
||||
if (this.group == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return group.members.stream()
|
||||
.filter(m -> m.role == Member.Role.ADMINISTRATOR)
|
||||
.map(m -> new RecipientAddress(ServiceId.ACI.parseOrNull(m.aciBytes),
|
||||
ServiceId.PNI.parseOrNull(m.pniBytes),
|
||||
null))
|
||||
.map(recipientResolver::resolveRecipient)
|
||||
public Set<RecipientId> getAdminMemberRecipientIds() {
|
||||
return this.getMembers()
|
||||
.stream()
|
||||
.filter(GroupMemberInfo::isAdmin)
|
||||
.map(GroupMemberInfo::getRecipientId)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
||||
public interface GroupMemberInfo {
|
||||
|
||||
RecipientId getRecipientId();
|
||||
|
||||
default boolean isAdmin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default String labelEmoji() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default String labelString() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
||||
public class GroupMemberInfoV1 implements GroupMemberInfo {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
|
||||
public GroupMemberInfoV1(final RecipientId recipientId) {
|
||||
this.recipientId = recipientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientId getRecipientId() {
|
||||
return this.recipientId;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.signal.core.models.ServiceId;
|
||||
import org.signal.storageservice.storage.protos.groups.Member;
|
||||
import org.signal.storageservice.storage.protos.groups.local.DecryptedMember;
|
||||
|
||||
public class GroupMemberInfoV2 implements GroupMemberInfo {
|
||||
|
||||
private final RecipientResolver recipientResolver;
|
||||
private final DecryptedMember member;
|
||||
|
||||
public GroupMemberInfoV2(final DecryptedMember member, final RecipientResolver recipientResolver) {
|
||||
this.recipientResolver = recipientResolver;
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientId getRecipientId() {
|
||||
return recipientResolver.resolveRecipient(ServiceId.ACI.parseOrThrow(member.aciBytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAdmin() {
|
||||
return member.role == Member.Role.ADMINISTRATOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String labelEmoji() {
|
||||
return member.labelEmoji.isEmpty() ? null : member.labelEmoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String labelString() {
|
||||
return member.labelString.isEmpty() ? null : member.labelString;
|
||||
}
|
||||
}
|
||||
@ -648,7 +648,7 @@ public class GroupStore {
|
||||
ON CONFLICT (group_id, recipient_id) DO NOTHING
|
||||
""".formatted(TABLE_GROUP_V1_MEMBER);
|
||||
try (final var statement = connection.prepareStatement(sqlInsertMember)) {
|
||||
for (final var recipient : groupV1.getMembers()) {
|
||||
for (final var recipient : groupV1.getMemberRecipientIds()) {
|
||||
statement.setLong(1, internalId);
|
||||
statement.setLong(2, recipient.id());
|
||||
statement.executeUpdate();
|
||||
|
||||
@ -133,6 +133,11 @@ public class KyberPreKeyStore implements SignalServiceKyberPreKeyStore {
|
||||
return getPreKey(keyId) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* When we mark Kyber pre-keys used, we want to keep a record of last resort tuples, which are deleted when the key
|
||||
* itself is deleted from this table via a cascading delete.
|
||||
* For non-last-resort keys, this method just deletes them like normal.
|
||||
*/
|
||||
@Override
|
||||
public void markKyberPreKeyUsed(
|
||||
final int kyberPreKeyId,
|
||||
|
||||
@ -14,8 +14,8 @@ all: $(MANPAGESRC)
|
||||
.PHONY: install
|
||||
install: all
|
||||
$(MKDIR) -p man1 man5
|
||||
for f in *.1; do $(GZIP) < "$$f" > man1/"$$f".gz ; done
|
||||
for f in *.5; do $(GZIP) < "$$f" > man5/"$$f".gz ; done
|
||||
for f in *.1; do $(GZIP) -n < "$$f" > man1/"$$f".gz ; done
|
||||
for f in *.5; do $(GZIP) -n < "$$f" > man5/"$$f".gz ; done
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
|
||||
@ -154,7 +154,7 @@ RESPONSE: `{"jsonrpc":"2.0","result":{"timestamp":999},"id":4}`
|
||||
|
||||
---
|
||||
|
||||
REQUEST: `{"jsonrpc":"2.0","method":"updateGroup","params":{"groupId":"GROUP_ID=","name":"new group name","members":["+ZZZ"],"link":"enabledWithApproval","setPermissionEditDetails":"only-admins"},"id":"someId"}`
|
||||
REQUEST: `{"jsonrpc":"2.0","method":"updateGroup","params":{"groupId":"GROUP_ID=","name":"new group name","members":["+ZZZ"],"link":"enabledWithApproval","setPermissionEditDetails":"only-admins","memberLabelEmoji":"😀","memberLabel":"My Label"},"id":"someId"}`
|
||||
|
||||
RESPONSE: `{"jsonrpc":"2.0","result":{"timestamp":9999},"id":"someId"}`
|
||||
|
||||
|
||||
@ -754,6 +754,12 @@ Groups where only admins can send messages are also called announcement groups
|
||||
Set expiration time of messages (seconds).
|
||||
To disable expiration set expiration time to 0.
|
||||
|
||||
*--member-label-emoji* EMOJI::
|
||||
Specify the emoji for the member label.
|
||||
|
||||
*--member-label* STRING::
|
||||
Specify the string for the member label.
|
||||
|
||||
=== quitGroup
|
||||
|
||||
Send a quit group message to all group members and remove self from member list.
|
||||
|
||||
34
reproducible-builds/README.md
Normal file
34
reproducible-builds/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Reproducible builds
|
||||
|
||||
This process lets you verify that the version of the app that was downloaded from the Githud Releases matches the source code in our public repository.
|
||||
|
||||
This is achieved by replicating the build environment as Docker images.
|
||||
|
||||
Currently, only the following binaries are reproducible:
|
||||
|
||||
- [x] JAR package (`signal-cli-XXX.tar.gz`)
|
||||
- [ ] Native binary (`signal-cli-XXX-Linux-native.tar.gz`)
|
||||
- [x] Rust client binary (`signal-cli-XXX-Linux-client.tar.gz`)
|
||||
|
||||
In the following section, we will use Signal version 0.14.0 as the reference example. Simply replace all occurrences of 0.14.0 with the version number you are about to verify.
|
||||
|
||||
## Step-by-step instructions
|
||||
|
||||
### 0. Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following installed:
|
||||
|
||||
- git
|
||||
- docker (or podman)
|
||||
|
||||
### 1. Verifying reproducibility
|
||||
|
||||
```bash
|
||||
git clone --depth 1 --branch v0.14.0 https://github.com/AsamK/signal-cli
|
||||
cd ./signal-cli
|
||||
./reproducible-builds/verify.sh
|
||||
```
|
||||
|
||||
If each one ends with `... matches!` for every binary (except the native one for now), you're good to go! You've successfully verified that the Github Release binaries were built from exactly the same code as is in the signal-cli git repository.
|
||||
|
||||
If you get `... doesn't match!`, it means something went wrong (except for the native one for now). Please [open an issue](https://github.com/AsamK/signal-cli/issues/new/choose).
|
||||
11
reproducible-builds/build.Containerfile
Normal file
11
reproducible-builds/build.Containerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM docker.io/azul/zulu-openjdk:25.0.2-jdk@sha256:0349494e05c22fe439e65be99771581b2bc428d89f07363b539389a11103fa5f
|
||||
ENV SOURCE_DATE_EPOCH=1767225600
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_CTYPE=en_US.UTF-8
|
||||
ARG SNAPSHOT=20260101T000000Z
|
||||
RUN echo "deb http://snapshot.ubuntu.com/ubuntu/${SNAPSHOT}/ jammy main" > /etc/apt/sources.list \
|
||||
&& echo "deb http://snapshot.ubuntu.com/ubuntu/${SNAPSHOT}/ jammy universe" >> /etc/apt/sources.list
|
||||
RUN apt update && apt install -y make asciidoc-base
|
||||
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
WORKDIR /signal-cli
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ]
|
||||
45
reproducible-builds/build.sh
Executable file
45
reproducible-builds/build.sh
Executable file
@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../"
|
||||
cd "$ROOT_DIR"
|
||||
rm -rf "$ROOT_DIR/dist"
|
||||
mkdir -p "$ROOT_DIR/dist"
|
||||
|
||||
if command -v podman >/dev/null; then
|
||||
ENGINE=podman
|
||||
USER=
|
||||
else
|
||||
ENGINE=docker
|
||||
USER="--user $(id -u):$(id -g)"
|
||||
fi
|
||||
|
||||
VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1)
|
||||
echo "$VERSION" >dist/VERSION
|
||||
|
||||
$ENGINE build -t signal-cli:build -f reproducible-builds/build.Containerfile .
|
||||
$ENGINE build -t signal-cli:native -f reproducible-builds/native.Containerfile .
|
||||
$ENGINE build -t signal-cli:client -f reproducible-builds/client.Containerfile .
|
||||
|
||||
# Build jar
|
||||
git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**'
|
||||
# shellcheck disable=SC2086
|
||||
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:build
|
||||
mv build/distributions/signal-cli-*.tar.gz dist/
|
||||
|
||||
# Build native-image
|
||||
git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**'
|
||||
# shellcheck disable=SC2086
|
||||
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:native
|
||||
mv build/signal-cli-*-Linux-native.tar.gz dist/
|
||||
|
||||
# Build rust client
|
||||
git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**'
|
||||
# shellcheck disable=SC2086
|
||||
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:client
|
||||
mv build/signal-cli-*-Linux-client.tar.gz dist/
|
||||
|
||||
ls -lsh dist/
|
||||
|
||||
echo -e "\e[32mBuild successful!\e[0m"
|
||||
7
reproducible-builds/client.Containerfile
Normal file
7
reproducible-builds/client.Containerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM docker.io/rust:1.93.1-slim-trixie@sha256:7f2c9f2f0dad8f4afa6faf5efa971e7e566398a36e54fb7684061407ea067058
|
||||
ENV SOURCE_DATE_EPOCH=1767225600
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_CTYPE=en_US.UTF-8
|
||||
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
WORKDIR /signal-cli
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "client" ]
|
||||
68
reproducible-builds/entrypoint.sh
Normal file
68
reproducible-builds/entrypoint.sh
Normal file
@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
echo "Build '$1' variant $VERSION ..."
|
||||
|
||||
function reset_file_dates() {
|
||||
find . -exec touch -m -d "@$SOURCE_DATE_EPOCH" {} \;
|
||||
}
|
||||
|
||||
reset_file_dates
|
||||
|
||||
if [ "$1" == "build" ]; then
|
||||
|
||||
./gradlew build \
|
||||
--no-daemon \
|
||||
--max-workers=1 \
|
||||
-Dkotlin.compiler.execution.strategy=in-process \
|
||||
--no-build-cache \
|
||||
-Dorg.gradle.caching=false \
|
||||
-Porg.gradle.java.installations.auto-download=false \
|
||||
-Porg.gradle.java.installations.auto-detect=false
|
||||
cd man
|
||||
make install
|
||||
cd ..
|
||||
reset_file_dates
|
||||
tar tf build/distributions/signal-cli-*.tar | head -n1 | sed 's|signal-cli-\([^/]*\)/.*|\1|'
|
||||
tar --mtime="@$SOURCE_DATE_EPOCH" --transform="flags=r;s|man|signal-cli-${VERSION}/man|" -rf "build/distributions/signal-cli-${VERSION}.tar" man/man{1,5}
|
||||
gzip -n -9 build/distributions/signal-cli-*.tar
|
||||
|
||||
elif [ "$1" == "native" ]; then
|
||||
|
||||
./gradlew nativeCompile \
|
||||
--no-daemon \
|
||||
--max-workers=1 \
|
||||
-Dkotlin.compiler.execution.strategy=in-process \
|
||||
--no-build-cache \
|
||||
-Dorg.gradle.caching=false \
|
||||
-Dgraalvm.native-image.build-time=2026-01-01T00:00:00Z \
|
||||
-Porg.gradle.java.installations.auto-download=false \
|
||||
-Porg.gradle.java.installations.auto-detect=false
|
||||
|
||||
strip --strip-all \
|
||||
--remove-section=.note.gnu.build-id \
|
||||
--remove-section=.comment \
|
||||
--remove-section=.gnu_debuglink \
|
||||
--remove-section=.annobin.notes \
|
||||
--remove-section=.gnu.build.attributes \
|
||||
--remove-section=.note.ABI-tag \
|
||||
build/native/nativeCompile/signal-cli
|
||||
|
||||
chmod +x build/native/nativeCompile/signal-cli
|
||||
reset_file_dates
|
||||
tar --mtime="@$SOURCE_DATE_EPOCH" -czf "build/signal-cli-${VERSION}-Linux-native.tar.gz" -C build/native/nativeCompile signal-cli
|
||||
|
||||
elif [ "$1" == "client" ]; then
|
||||
|
||||
cd client
|
||||
cargo build --release --locked
|
||||
cd ..
|
||||
chmod +x client/target/release/signal-cli-client
|
||||
mkdir -p build
|
||||
tar --mtime="@$SOURCE_DATE_EPOCH" -czf "build/signal-cli-${VERSION}-Linux-client.tar.gz" -C client/target/release signal-cli-client
|
||||
|
||||
else
|
||||
echo "Unknown build variant '$1'"
|
||||
exit 1
|
||||
fi
|
||||
7
reproducible-builds/native.Containerfile
Normal file
7
reproducible-builds/native.Containerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM container-registry.oracle.com/graalvm/native-image:25.0.2@sha256:e8c5ec4f256bf958f327aea060e1424aa87f63114aeb4a4318a0ac169bbdb9a1
|
||||
ENV SOURCE_DATE_EPOCH=1767225600
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_CTYPE=en_US.UTF-8
|
||||
COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
WORKDIR /signal-cli
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "native" ]
|
||||
41
reproducible-builds/verify.sh
Executable file
41
reproducible-builds/verify.sh
Executable file
@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../"
|
||||
cd "$ROOT_DIR"
|
||||
rm -rf "$ROOT_DIR/github"
|
||||
mkdir -p "$ROOT_DIR/github"
|
||||
|
||||
VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1)
|
||||
|
||||
echo "Download latest release from GitHub..."
|
||||
|
||||
curl -L --fail "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}.tar.gz" -o "github/signal-cli-${VERSION}.tar.gz"
|
||||
curl -L --fail "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}-Linux-native.tar.gz" -o "github/signal-cli-${VERSION}-Linux-native.tar.gz"
|
||||
curl -L --fail "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}-Linux-client.tar.gz" -o "github/signal-cli-${VERSION}-Linux-client.tar.gz"
|
||||
|
||||
./reproducible-builds/build.sh
|
||||
|
||||
rm -f {github,dist}/VERSION
|
||||
|
||||
echo "commit: $(git rev-parse HEAD)"
|
||||
|
||||
echo "sha256 hashes of GitHub release:"
|
||||
sha256sum github/*
|
||||
echo "sha256 hashes of locally built files:"
|
||||
sha256sum dist/*
|
||||
|
||||
reproducible=true
|
||||
for file in $(cd github && find . -type f); do
|
||||
if diff "github/$file" "dist/$file" >/dev/null 2>&1; then
|
||||
echo -e "\e[32m[+] '$(basename "$file")' matches!\e[0m"
|
||||
else
|
||||
echo -e "\e[31m[-] '$(basename "$file")' doesn't match!\e[0m"
|
||||
reproducible=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$reproducible" = false ]; then
|
||||
exit 1
|
||||
fi
|
||||
@ -1,5 +1,7 @@
|
||||
package org.asamk.signal.commands;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
import net.sourceforge.argparse4j.impl.Arguments;
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
@ -7,6 +9,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
|
||||
import org.asamk.signal.commands.exceptions.CommandException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.api.Group;
|
||||
import org.asamk.signal.manager.api.GroupMember;
|
||||
import org.asamk.signal.manager.api.RecipientAddress;
|
||||
import org.asamk.signal.output.JsonWriter;
|
||||
import org.asamk.signal.output.OutputWriter;
|
||||
@ -37,33 +40,55 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
||||
subparser.addArgument("-g", "--group-id").help("Specify one or more group IDs to show.").nargs("*");
|
||||
}
|
||||
|
||||
private static Set<String> resolveMembers(Set<RecipientAddress> addresses) {
|
||||
private static Set<String> resolveMembers(Set<GroupMember> addresses) {
|
||||
return addresses.stream()
|
||||
.map(m -> m.recipientAddress().getLegacyIdentifier() + (m.isAdmin() ? "{ADMIN}" : "") + (
|
||||
m.labelEmoji() != null || m.label() != null ? "(" + (
|
||||
m.labelEmoji() != null ? m.labelEmoji() : ""
|
||||
) + (
|
||||
m.label() != null ? m.label() : ""
|
||||
) + ")" : ""
|
||||
))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static Set<String> resolveMemberAddress(Set<RecipientAddress> addresses) {
|
||||
return addresses.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static Set<JsonGroupMember> resolveJsonMembers(Set<RecipientAddress> addresses) {
|
||||
private static Set<JsonGroupMemberAddress> resolveJsonMembers(Set<RecipientAddress> addresses) {
|
||||
return addresses.stream()
|
||||
.map(address -> new JsonGroupMember(address.number().orElse(null),
|
||||
.map(address -> new JsonGroupMemberAddress(address.number().orElse(null),
|
||||
address.uuid().map(UUID::toString).orElse(null)))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static Set<JsonGroupMember> resolveFullJsonMembers(Set<GroupMember> addresses) {
|
||||
return addresses.stream().map(member -> {
|
||||
final var address = member.recipientAddress();
|
||||
return new JsonGroupMember(address.number().orElse(null),
|
||||
address.uuid().map(UUID::toString).orElse(null),
|
||||
member.isAdmin(),
|
||||
member.labelEmoji(),
|
||||
member.label());
|
||||
}).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static void printGroupPlainText(PlainTextWriter writer, Group group, boolean detailed) {
|
||||
if (detailed) {
|
||||
final var groupInviteLink = group.groupInviteLinkUrl();
|
||||
|
||||
writer.println(
|
||||
"Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Banned: {} Message expiration: {} Link: {}",
|
||||
"Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Banned: {} Message expiration: {} Link: {}",
|
||||
group.groupId().toBase64(),
|
||||
group.title(),
|
||||
group.description(),
|
||||
group.isMember(),
|
||||
group.isBlocked(),
|
||||
resolveMembers(group.members()),
|
||||
resolveMembers(group.pendingMembers()),
|
||||
resolveMembers(group.requestingMembers()),
|
||||
resolveMembers(group.adminMembers()),
|
||||
resolveMembers(group.bannedMembers()),
|
||||
resolveMemberAddress(group.pendingMembers()),
|
||||
resolveMemberAddress(group.requestingMembers()),
|
||||
resolveMemberAddress(group.bannedMembers()),
|
||||
group.messageExpirationTimer() == 0 ? "disabled" : group.messageExpirationTimer() + "s",
|
||||
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
|
||||
} else {
|
||||
@ -96,10 +121,14 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
||||
group.isMember(),
|
||||
group.isBlocked(),
|
||||
group.messageExpirationTimer(),
|
||||
resolveJsonMembers(group.members()),
|
||||
resolveFullJsonMembers(group.members()),
|
||||
resolveJsonMembers(group.pendingMembers()),
|
||||
resolveJsonMembers(group.requestingMembers()),
|
||||
resolveJsonMembers(group.adminMembers()),
|
||||
resolveJsonMembers(group.members()
|
||||
.stream()
|
||||
.filter(GroupMember::isAdmin)
|
||||
.map(GroupMember::recipientAddress)
|
||||
.collect(Collectors.toSet())),
|
||||
resolveJsonMembers(group.bannedMembers()),
|
||||
group.permissionAddMember().name(),
|
||||
group.permissionEditDetails().name(),
|
||||
@ -125,15 +154,23 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
||||
boolean isBlocked,
|
||||
int messageExpirationTime,
|
||||
Set<JsonGroupMember> members,
|
||||
Set<JsonGroupMember> pendingMembers,
|
||||
Set<JsonGroupMember> requestingMembers,
|
||||
Set<JsonGroupMember> admins,
|
||||
Set<JsonGroupMember> banned,
|
||||
Set<JsonGroupMemberAddress> pendingMembers,
|
||||
Set<JsonGroupMemberAddress> requestingMembers,
|
||||
@Deprecated Set<JsonGroupMemberAddress> admins,
|
||||
Set<JsonGroupMemberAddress> banned,
|
||||
String permissionAddMember,
|
||||
String permissionEditDetails,
|
||||
String permissionSendMessage,
|
||||
String groupInviteLink
|
||||
) {}
|
||||
|
||||
private record JsonGroupMember(String number, String uuid) {}
|
||||
private record JsonGroupMemberAddress(String number, String uuid) {}
|
||||
|
||||
private record JsonGroupMember(
|
||||
String number,
|
||||
String uuid,
|
||||
boolean isAdmin,
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL) String labelEmoji,
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL) String label
|
||||
) {}
|
||||
}
|
||||
|
||||
@ -75,6 +75,8 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
|
||||
.choices("every-member", "only-admins");
|
||||
|
||||
subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)");
|
||||
subparser.addArgument("--member-label-emoji").help("Specify the emoji for the member label.");
|
||||
subparser.addArgument("--member-label").help("Specify the string for the member label.");
|
||||
}
|
||||
|
||||
GroupLinkState getGroupLinkState(String value) throws UserErrorException {
|
||||
@ -126,6 +128,8 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
|
||||
var groupAddMemberPermission = getGroupPermission(ns.getString("set-permission-add-member"));
|
||||
var groupEditDetailsPermission = getGroupPermission(ns.getString("set-permission-edit-details"));
|
||||
var groupSendMessagesPermission = getGroupPermission(ns.getString("set-permission-send-messages"));
|
||||
var memberLabelEmoji = ns.getString("member-label-emoji");
|
||||
var memberLabelString = ns.getString("member-label");
|
||||
|
||||
try {
|
||||
boolean isNewGroup = false;
|
||||
@ -159,6 +163,8 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
|
||||
.withIsAnnouncementGroup(groupSendMessagesPermission == null
|
||||
? null
|
||||
: groupSendMessagesPermission == GroupPermission.ONLY_ADMINS)
|
||||
.withLabelEmoji(memberLabelEmoji)
|
||||
.withLabelString(memberLabelString)
|
||||
.build());
|
||||
if (results != null) {
|
||||
if (groupMessageResults == null) {
|
||||
|
||||
@ -12,6 +12,7 @@ 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.GroupMember;
|
||||
import org.asamk.signal.manager.api.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.api.GroupPermission;
|
||||
import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
||||
@ -834,24 +835,22 @@ public class DbusManagerImpl implements Manager {
|
||||
final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
|
||||
final var id = (byte[]) group.get("Id").getValue();
|
||||
try {
|
||||
final var admins = new HashSet<>(((List<String>) group.get("Admins").getValue()));
|
||||
return new Group(GroupId.unknownVersion(id),
|
||||
(String) group.get("Name").getValue(),
|
||||
(String) group.get("Description").getValue(),
|
||||
GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
|
||||
((List<String>) group.get("Members").getValue()).stream()
|
||||
.map(m -> new RecipientAddress(m))
|
||||
.map(m -> new GroupMember(new RecipientAddress(m), admins.contains(m), null, null))
|
||||
.collect(Collectors.toSet()),
|
||||
((List<String>) group.get("PendingMembers").getValue()).stream()
|
||||
.map(m -> new RecipientAddress(m))
|
||||
.map(RecipientAddress::new)
|
||||
.collect(Collectors.toSet()),
|
||||
((List<String>) group.get("RequestingMembers").getValue()).stream()
|
||||
.map(m -> new RecipientAddress(m))
|
||||
.collect(Collectors.toSet()),
|
||||
((List<String>) group.get("Admins").getValue()).stream()
|
||||
.map(m -> new RecipientAddress(m))
|
||||
.map(RecipientAddress::new)
|
||||
.collect(Collectors.toSet()),
|
||||
((List<String>) group.get("Banned").getValue()).stream()
|
||||
.map(m -> new RecipientAddress(m))
|
||||
.map(RecipientAddress::new)
|
||||
.collect(Collectors.toSet()),
|
||||
(boolean) group.get("IsBlocked").getValue(),
|
||||
(int) group.get("MessageExpirationTimer").getValue(),
|
||||
|
||||
@ -10,6 +10,7 @@ import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.api.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.api.GroupLinkState;
|
||||
import org.asamk.signal.manager.api.GroupMember;
|
||||
import org.asamk.signal.manager.api.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.api.GroupPermission;
|
||||
import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
||||
@ -624,7 +625,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||
if (group == null) {
|
||||
return List.of();
|
||||
} else {
|
||||
final var members = group.members();
|
||||
final var members = group.members().stream().map(GroupMember::recipientAddress).collect(Collectors.toSet());
|
||||
return getRecipientStrings(members);
|
||||
}
|
||||
}
|
||||
@ -1300,13 +1301,20 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||
() -> getGroup().messageExpirationTimer(),
|
||||
this::setMessageExpirationTime),
|
||||
new DbusProperty<>("Members",
|
||||
() -> new Variant<>(getRecipientStrings(getGroup().members()), "as")),
|
||||
() -> new Variant<>(getRecipientStrings(getGroup().members()
|
||||
.stream()
|
||||
.map(GroupMember::recipientAddress)
|
||||
.collect(Collectors.toSet())), "as")),
|
||||
new DbusProperty<>("PendingMembers",
|
||||
() -> new Variant<>(getRecipientStrings(getGroup().pendingMembers()), "as")),
|
||||
new DbusProperty<>("RequestingMembers",
|
||||
() -> new Variant<>(getRecipientStrings(getGroup().requestingMembers()), "as")),
|
||||
new DbusProperty<>("Admins",
|
||||
() -> new Variant<>(getRecipientStrings(getGroup().adminMembers()), "as")),
|
||||
() -> new Variant<>(getRecipientStrings(getGroup().members()
|
||||
.stream()
|
||||
.filter(GroupMember::isAdmin)
|
||||
.map(GroupMember::recipientAddress)
|
||||
.collect(Collectors.toSet())), "as")),
|
||||
new DbusProperty<>("Banned",
|
||||
() -> new Variant<>(getRecipientStrings(getGroup().bannedMembers()), "as")),
|
||||
new DbusProperty<>("PermissionAddMember",
|
||||
|
||||
@ -2048,6 +2048,19 @@
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.commands.ListGroupsCommand$JsonGroupMemberAddress",
|
||||
"methods": [
|
||||
{
|
||||
"name": "number",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.commands.ListIdentitiesCommand$JsonIdentity",
|
||||
"allDeclaredFields": true,
|
||||
@ -7793,6 +7806,18 @@
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.push.GroupMismatchedDevices[]"
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.push.GroupPatchResponse",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": [
|
||||
"java.lang.Integer",
|
||||
"java.lang.String"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.push.GroupStaleDevices",
|
||||
"allDeclaredFields": true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user