application
eclipse
`check-lib-versions`
- id("org.graalvm.buildtools.native") version "0.9.5"
+ id("org.graalvm.buildtools.native") version "0.9.6"
}
version = "0.9.0"
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 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.
#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# 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/master/subprojects/plugins/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
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
location of your Java installation."
fi
else
- JAVACMD="java"
+ JAVACMD=java
which java >/dev/null 2>&1 || 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
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# 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
+# 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" = "true" -o "$msys" = "true" ] ; 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
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# 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\""
+ 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
- i=`expr $i + 1`
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
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
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# 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.
+#
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
}
dependencies {
- api("com.github.turasa:signal-service-java:2.15.3_unofficial_28")
+ api("com.github.turasa:signal-service-java:2.15.3_unofficial_29")
implementation("com.google.protobuf:protobuf-javalite:3.10.0")
implementation("org.bouncycastle:bcprov-jdk15on:1.69")
implementation("org.slf4j:slf4j-api:1.7.30")
public URI createDeviceLinkUri() {
final var deviceKeyString = Base64.getEncoder().encodeToString(deviceKey.serialize()).replace("=", "");
try {
- return new URI("tsdevice:/?uuid="
+ return new URI("sgnl://linkdevice?uuid="
+ URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8)
+ "&pub_key="
+ URLEncoder.encode(deviceKeyString, StandardCharsets.UTF_8));
import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
-import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupNotFoundException;
-import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
-import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
) throws IOException, AttachmentInvalidException;
SendGroupMessageResults updateGroup(
- GroupId groupId,
- String name,
- String description,
- Set<RecipientIdentifier.Single> members,
- Set<RecipientIdentifier.Single> removeMembers,
- Set<RecipientIdentifier.Single> admins,
- Set<RecipientIdentifier.Single> removeAdmins,
- boolean resetGroupLink,
- GroupLinkState groupLinkState,
- GroupPermission addMemberPermission,
- GroupPermission editDetailsPermission,
- File avatarFile,
- Integer expirationTimer,
- Boolean isAnnouncementGroup
+ final GroupId groupId, final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException;
Pair<GroupId, SendGroupMessageResults> joinGroup(
void setGroupBlocked(
GroupId groupId, boolean blocked
- ) throws GroupNotFoundException, IOException;
+ ) throws GroupNotFoundException, IOException, NotMasterDeviceException;
void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer
boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient);
- String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey);
-
SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address);
@Override
import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
-import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupNotFoundException;
-import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.helper.ContactHelper;
import org.asamk.signal.manager.helper.GroupHelper;
import org.asamk.signal.manager.helper.GroupV2Helper;
+import org.asamk.signal.manager.helper.IdentityHelper;
import org.asamk.signal.manager.helper.IncomingMessageHandler;
import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.PreKeyHelper;
import org.asamk.signal.manager.storage.stickers.StickerPackId;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.StickerUtils;
-import org.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.ECPublicKey;
-import org.whispersystems.libsignal.fingerprint.Fingerprint;
-import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
-import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SignatureException;
-import java.util.Arrays;
import java.util.Collection;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantLock;
-import java.util.function.Function;
import java.util.stream.Collectors;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
private final ContactHelper contactHelper;
private final IncomingMessageHandler incomingMessageHandler;
private final PreKeyHelper preKeyHelper;
+ private final IdentityHelper identityHelper;
private final Context context;
private boolean hasCaughtUpWithOldMessages = false;
this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore);
this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
- final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
- account.getProfileStore()::getProfileKey,
- this::getRecipientProfile,
- this::getSenderCertificate);
+ final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account,
+ dependencies,
+ account::getProfileKey,
+ this::getRecipientProfile);
this.profileHelper = new ProfileHelper(account,
dependencies,
avatarStore,
- account.getProfileStore()::getProfileKey,
unidentifiedAccessHelper::getAccessFor,
this::resolveSignalServiceAddress);
final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
syncHelper,
this::getRecipientProfile,
jobExecutor);
+ this.identityHelper = new IdentityHelper(account,
+ dependencies,
+ this::resolveSignalServiceAddress,
+ syncHelper,
+ profileHelper);
}
@Override
.map(account.getRecipientStore()::resolveRecipientAddress)
.collect(Collectors.toSet()),
groupInfo.isBlocked(),
- groupInfo.getMessageExpirationTime(),
- groupInfo.isAnnouncementGroup(),
- groupInfo.isMember(account.getSelfRecipientId()));
+ groupInfo.getMessageExpirationTimer(),
+ groupInfo.getPermissionAddMember(),
+ groupInfo.getPermissionEditDetails(),
+ groupInfo.getPermissionSendMessage(),
+ groupInfo.isMember(account.getSelfRecipientId()),
+ groupInfo.isAdmin(account.getSelfRecipientId()));
}
@Override
@Override
public SendGroupMessageResults updateGroup(
- GroupId groupId,
- String name,
- String description,
- Set<RecipientIdentifier.Single> members,
- Set<RecipientIdentifier.Single> removeMembers,
- Set<RecipientIdentifier.Single> admins,
- Set<RecipientIdentifier.Single> removeAdmins,
- boolean resetGroupLink,
- GroupLinkState groupLinkState,
- GroupPermission addMemberPermission,
- GroupPermission editDetailsPermission,
- File avatarFile,
- Integer expirationTimer,
- Boolean isAnnouncementGroup
+ final GroupId groupId, final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
return groupHelper.updateGroup(groupId,
- name,
- description,
- members == null ? null : resolveRecipients(members),
- removeMembers == null ? null : resolveRecipients(removeMembers),
- admins == null ? null : resolveRecipients(admins),
- removeAdmins == null ? null : resolveRecipients(removeAdmins),
- resetGroupLink,
- groupLinkState,
- addMemberPermission,
- editDetailsPermission,
- avatarFile,
- expirationTimer,
- isAnnouncementGroup);
+ updateGroup.getName(),
+ updateGroup.getDescription(),
+ updateGroup.getMembers() == null ? null : resolveRecipients(updateGroup.getMembers()),
+ updateGroup.getRemoveMembers() == null ? null : resolveRecipients(updateGroup.getRemoveMembers()),
+ updateGroup.getAdmins() == null ? null : resolveRecipients(updateGroup.getAdmins()),
+ updateGroup.getRemoveAdmins() == null ? null : resolveRecipients(updateGroup.getRemoveAdmins()),
+ updateGroup.isResetGroupLink(),
+ updateGroup.getGroupLinkState(),
+ updateGroup.getAddMemberPermission(),
+ updateGroup.getEditDetailsPermission(),
+ updateGroup.getAvatarFile(),
+ updateGroup.getExpirationTimer(),
+ updateGroup.getIsAnnouncementGroup());
}
@Override
@Override
public void setGroupBlocked(
final GroupId groupId, final boolean blocked
- ) throws GroupNotFoundException, IOException {
+ ) throws GroupNotFoundException, IOException, NotMasterDeviceException {
+ if (!account.isMasterDevice()) {
+ throw new NotMasterDeviceException();
+ }
groupHelper.setGroupBlocked(groupId, blocked);
// TODO cycle our profile key
syncHelper.sendBlockedList();
}
}
- private byte[] getSenderCertificate() {
- byte[] certificate;
- try {
- if (account.isPhoneNumberShared()) {
- certificate = dependencies.getAccountManager().getSenderCertificate();
- } else {
- certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
- }
- } catch (IOException e) {
- logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
- return null;
- }
- // TODO cache for a day
- return certificate;
- }
-
private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException {
final var address = resolveSignalServiceAddress(recipientId);
if (!address.getNumber().isPresent()) {
return toGroup(groupHelper.getGroup(groupId));
}
- public GroupInfo getGroupInfo(GroupId groupId) {
+ private GroupInfo getGroupInfo(GroupId groupId) {
return groupHelper.getGroup(groupId);
}
final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId());
return new Identity(address,
identityInfo.getIdentityKey(),
- computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()),
- computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()),
+ identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()),
+ identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(),
+ identityInfo.getIdentityKey()).getSerialized(),
identityInfo.getTrustLevel(),
identityInfo.getDateAdded());
}
} catch (UnregisteredUserException e) {
return false;
}
- return trustIdentity(recipientId,
- identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
- TrustLevel.TRUSTED_VERIFIED);
+ return identityHelper.trustIdentityVerified(recipientId, fingerprint);
}
/**
} catch (UnregisteredUserException e) {
return false;
}
- var address = resolveSignalServiceAddress(recipientId);
- return trustIdentity(recipientId,
- identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
- TrustLevel.TRUSTED_VERIFIED);
+ return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
}
/**
} catch (UnregisteredUserException e) {
return false;
}
- var address = resolveSignalServiceAddress(recipientId);
- return trustIdentity(recipientId, identityKey -> {
- final var fingerprint = computeSafetyNumberFingerprint(address, identityKey);
- try {
- return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber);
- } catch (FingerprintVersionMismatchException | FingerprintParsingException e) {
- return false;
- }
- }, TrustLevel.TRUSTED_VERIFIED);
+ return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber);
}
/**
} catch (UnregisteredUserException e) {
return false;
}
- return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
- }
-
- private boolean trustIdentity(
- RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
- ) {
- var identity = account.getIdentityKeyStore().getIdentity(recipientId);
- if (identity == null) {
- return false;
- }
-
- if (!verifier.apply(identity.getIdentityKey())) {
- return false;
- }
-
- account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
- try {
- var address = resolveSignalServiceAddress(recipientId);
- syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
- } catch (IOException e) {
- logger.warn("Failed to send verification sync message: {}", e.getMessage());
- }
-
- return true;
+ return identityHelper.trustIdentityAllKeys(recipientId);
}
private void handleIdentityFailure(
final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure
) {
- final var identityKey = identityFailure.getIdentityKey();
- if (identityKey != null) {
- final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
- if (newIdentity) {
- account.getSessionStore().archiveSessions(recipientId);
- }
- } else {
- // Retrieve profile to get the current identity key from the server
- profileHelper.refreshRecipientProfile(recipientId);
- }
- }
-
- @Override
- public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
- final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey);
- return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText();
- }
-
- private byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
- final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey);
- return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized();
- }
-
- private Fingerprint computeSafetyNumberFingerprint(
- final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
- ) {
- return Utils.computeSafetyNumber(capabilities.isUuid(),
- account.getSelfAddress(),
- account.getIdentityKeyPair().getPublicKey(),
- theirAddress,
- theirIdentityKey);
+ this.identityHelper.handleIdentityFailure(recipientId, identityFailure);
}
@Override
}
account = null;
}
-
}
if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
logger.warn("Received too many mismatch device errors, forcing new websockets.");
signalWebSocket.forceNewWebSockets();
+ signalWebSocket.connect();
}
}
}
+ " needed by: "
+ keepAliveRequiredSinceTime);
signalWebSocket.forceNewWebSockets();
+ signalWebSocket.connect();
} else {
signalWebSocket.sendKeepAlive();
}
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import java.util.Set;
private final Set<RecipientAddress> requestingMembers;
private final Set<RecipientAddress> adminMembers;
private final boolean isBlocked;
- private final int messageExpirationTime;
- private final boolean isAnnouncementGroup;
+ private final int messageExpirationTimer;
+
+ private final GroupPermission permissionAddMember;
+ private final GroupPermission permissionEditDetails;
+ private final GroupPermission permissionSendMessage;
private final boolean isMember;
+ private final boolean isAdmin;
public Group(
final GroupId groupId,
final Set<RecipientAddress> requestingMembers,
final Set<RecipientAddress> adminMembers,
final boolean isBlocked,
- final int messageExpirationTime,
- final boolean isAnnouncementGroup,
- final boolean isMember
+ final int messageExpirationTimer,
+ final GroupPermission permissionAddMember,
+ final GroupPermission permissionEditDetails,
+ final GroupPermission permissionSendMessage,
+ final boolean isMember,
+ final boolean isAdmin
) {
this.groupId = groupId;
this.title = title;
this.requestingMembers = requestingMembers;
this.adminMembers = adminMembers;
this.isBlocked = isBlocked;
- this.messageExpirationTime = messageExpirationTime;
- this.isAnnouncementGroup = isAnnouncementGroup;
+ this.messageExpirationTimer = messageExpirationTimer;
+ this.permissionAddMember = permissionAddMember;
+ this.permissionEditDetails = permissionEditDetails;
+ this.permissionSendMessage = permissionSendMessage;
this.isMember = isMember;
+ this.isAdmin = isAdmin;
}
public GroupId getGroupId() {
return isBlocked;
}
- public int getMessageExpirationTime() {
- return messageExpirationTime;
+ public int getMessageExpirationTimer() {
+ return messageExpirationTimer;
+ }
+
+ public GroupPermission getPermissionAddMember() {
+ return permissionAddMember;
}
- public boolean isAnnouncementGroup() {
- return isAnnouncementGroup;
+ public GroupPermission getPermissionEditDetails() {
+ return permissionEditDetails;
+ }
+
+ public GroupPermission getPermissionSendMessage() {
+ return permissionSendMessage;
}
public boolean isMember() {
return isMember;
}
+
+ public boolean isAdmin() {
+ return isAdmin;
+ }
}
--- /dev/null
+package org.asamk.signal.manager.api;
+
+import org.asamk.signal.manager.groups.GroupLinkState;
+import org.asamk.signal.manager.groups.GroupPermission;
+
+import java.io.File;
+import java.util.Set;
+
+public class UpdateGroup {
+
+ private final String name;
+ private final String description;
+ private final Set<RecipientIdentifier.Single> members;
+ private final Set<RecipientIdentifier.Single> removeMembers;
+ private final Set<RecipientIdentifier.Single> admins;
+ private final Set<RecipientIdentifier.Single> removeAdmins;
+ private final boolean resetGroupLink;
+ private final GroupLinkState groupLinkState;
+ private final GroupPermission addMemberPermission;
+ private final GroupPermission editDetailsPermission;
+ private final File avatarFile;
+ private final Integer expirationTimer;
+ private final Boolean isAnnouncementGroup;
+
+ private UpdateGroup(final Builder builder) {
+ name = builder.name;
+ description = builder.description;
+ members = builder.members;
+ removeMembers = builder.removeMembers;
+ admins = builder.admins;
+ removeAdmins = builder.removeAdmins;
+ resetGroupLink = builder.resetGroupLink;
+ groupLinkState = builder.groupLinkState;
+ addMemberPermission = builder.addMemberPermission;
+ editDetailsPermission = builder.editDetailsPermission;
+ avatarFile = builder.avatarFile;
+ expirationTimer = builder.expirationTimer;
+ isAnnouncementGroup = builder.isAnnouncementGroup;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static Builder newBuilder(final UpdateGroup copy) {
+ Builder builder = new Builder();
+ builder.name = copy.getName();
+ builder.description = copy.getDescription();
+ builder.members = copy.getMembers();
+ builder.removeMembers = copy.getRemoveMembers();
+ builder.admins = copy.getAdmins();
+ builder.removeAdmins = copy.getRemoveAdmins();
+ builder.resetGroupLink = copy.isResetGroupLink();
+ builder.groupLinkState = copy.getGroupLinkState();
+ builder.addMemberPermission = copy.getAddMemberPermission();
+ builder.editDetailsPermission = copy.getEditDetailsPermission();
+ builder.avatarFile = copy.getAvatarFile();
+ builder.expirationTimer = copy.getExpirationTimer();
+ builder.isAnnouncementGroup = copy.getIsAnnouncementGroup();
+ return builder;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Set<RecipientIdentifier.Single> getMembers() {
+ return members;
+ }
+
+ public Set<RecipientIdentifier.Single> getRemoveMembers() {
+ return removeMembers;
+ }
+
+ public Set<RecipientIdentifier.Single> getAdmins() {
+ return admins;
+ }
+
+ public Set<RecipientIdentifier.Single> getRemoveAdmins() {
+ return removeAdmins;
+ }
+
+ public boolean isResetGroupLink() {
+ return resetGroupLink;
+ }
+
+ public GroupLinkState getGroupLinkState() {
+ return groupLinkState;
+ }
+
+ public GroupPermission getAddMemberPermission() {
+ return addMemberPermission;
+ }
+
+ public GroupPermission getEditDetailsPermission() {
+ return editDetailsPermission;
+ }
+
+ public File getAvatarFile() {
+ return avatarFile;
+ }
+
+ public Integer getExpirationTimer() {
+ return expirationTimer;
+ }
+
+ public Boolean getIsAnnouncementGroup() {
+ return isAnnouncementGroup;
+ }
+
+ public static final class Builder {
+
+ private String name;
+ private String description;
+ private Set<RecipientIdentifier.Single> members;
+ private Set<RecipientIdentifier.Single> removeMembers;
+ private Set<RecipientIdentifier.Single> admins;
+ private Set<RecipientIdentifier.Single> removeAdmins;
+ private boolean resetGroupLink;
+ private GroupLinkState groupLinkState;
+ private GroupPermission addMemberPermission;
+ private GroupPermission editDetailsPermission;
+ private File avatarFile;
+ private Integer expirationTimer;
+ private Boolean isAnnouncementGroup;
+
+ private Builder() {
+ }
+
+ public Builder withName(final String val) {
+ name = val;
+ return this;
+ }
+
+ public Builder withDescription(final String val) {
+ description = val;
+ return this;
+ }
+
+ public Builder withMembers(final Set<RecipientIdentifier.Single> val) {
+ members = val;
+ return this;
+ }
+
+ public Builder withRemoveMembers(final Set<RecipientIdentifier.Single> val) {
+ removeMembers = val;
+ return this;
+ }
+
+ public Builder withAdmins(final Set<RecipientIdentifier.Single> val) {
+ admins = val;
+ return this;
+ }
+
+ public Builder withRemoveAdmins(final Set<RecipientIdentifier.Single> val) {
+ removeAdmins = val;
+ return this;
+ }
+
+ public Builder withResetGroupLink(final boolean val) {
+ resetGroupLink = val;
+ return this;
+ }
+
+ public Builder withGroupLinkState(final GroupLinkState val) {
+ groupLinkState = val;
+ return this;
+ }
+
+ public Builder withAddMemberPermission(final GroupPermission val) {
+ addMemberPermission = val;
+ return this;
+ }
+
+ public Builder withEditDetailsPermission(final GroupPermission val) {
+ editDetailsPermission = val;
+ return this;
+ }
+
+ public Builder withAvatarFile(final File val) {
+ avatarFile = val;
+ return this;
+ }
+
+ public Builder withExpirationTimer(final Integer val) {
+ expirationTimer = val;
+ return this;
+ }
+
+ public Builder withIsAnnouncementGroup(final Boolean val) {
+ isAnnouncementGroup = val;
+ return this;
+ }
+
+ public UpdateGroup build() {
+ return new UpdateGroup(this);
+ }
+ }
+}
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
- .withExpiration(g.getMessageExpirationTime());
+ .withExpiration(g.getMessageExpirationTimer());
}
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
.withSignedGroupChange(signedGroupChange);
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
- .withExpiration(g.getMessageExpirationTime());
+ .withExpiration(g.getMessageExpirationTimer());
}
private SendGroupMessageResults sendUpdateGroupV2Message(
--- /dev/null
+package org.asamk.signal.manager.helper;
+
+import org.asamk.signal.manager.SignalDependencies;
+import org.asamk.signal.manager.TrustLevel;
+import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.asamk.signal.manager.util.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.fingerprint.Fingerprint;
+import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
+import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
+import org.whispersystems.libsignal.fingerprint.ScannableFingerprint;
+import org.whispersystems.signalservice.api.messages.SendMessageResult;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.function.Function;
+
+import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
+
+public class IdentityHelper {
+
+ private final static Logger logger = LoggerFactory.getLogger(IdentityHelper.class);
+
+ private final SignalAccount account;
+ private final SignalDependencies dependencies;
+ private final SignalServiceAddressResolver addressResolver;
+ private final SyncHelper syncHelper;
+ private final ProfileHelper profileHelper;
+
+ public IdentityHelper(
+ final SignalAccount account,
+ final SignalDependencies dependencies,
+ final SignalServiceAddressResolver addressResolver,
+ final SyncHelper syncHelper,
+ final ProfileHelper profileHelper
+ ) {
+ this.account = account;
+ this.dependencies = dependencies;
+ this.addressResolver = addressResolver;
+ this.syncHelper = syncHelper;
+ this.profileHelper = profileHelper;
+ }
+
+ public boolean trustIdentityVerified(RecipientId recipientId, byte[] fingerprint) {
+ return trustIdentity(recipientId,
+ identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
+ TrustLevel.TRUSTED_VERIFIED);
+ }
+
+ public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, String safetyNumber) {
+ return trustIdentity(recipientId,
+ identityKey -> safetyNumber.equals(computeSafetyNumber(recipientId, identityKey)),
+ TrustLevel.TRUSTED_VERIFIED);
+ }
+
+ public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, byte[] safetyNumber) {
+ return trustIdentity(recipientId, identityKey -> {
+ final var fingerprint = computeSafetyNumberForScanning(recipientId, identityKey);
+ try {
+ return fingerprint != null && fingerprint.compareTo(safetyNumber);
+ } catch (FingerprintVersionMismatchException | FingerprintParsingException e) {
+ return false;
+ }
+ }, TrustLevel.TRUSTED_VERIFIED);
+ }
+
+ public boolean trustIdentityAllKeys(RecipientId recipientId) {
+ return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
+ }
+
+ public String computeSafetyNumber(RecipientId recipientId, IdentityKey theirIdentityKey) {
+ var address = addressResolver.resolveSignalServiceAddress(recipientId);
+ final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey);
+ return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText();
+ }
+
+ public ScannableFingerprint computeSafetyNumberForScanning(RecipientId recipientId, IdentityKey theirIdentityKey) {
+ var address = addressResolver.resolveSignalServiceAddress(recipientId);
+ final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey);
+ return fingerprint == null ? null : fingerprint.getScannableFingerprint();
+ }
+
+ private Fingerprint computeSafetyNumberFingerprint(
+ final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
+ ) {
+ return Utils.computeSafetyNumber(capabilities.isUuid(),
+ account.getSelfAddress(),
+ account.getIdentityKeyPair().getPublicKey(),
+ theirAddress,
+ theirIdentityKey);
+ }
+
+ private boolean trustIdentity(
+ RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
+ ) {
+ var identity = account.getIdentityKeyStore().getIdentity(recipientId);
+ if (identity == null) {
+ return false;
+ }
+
+ if (!verifier.apply(identity.getIdentityKey())) {
+ return false;
+ }
+
+ account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
+ try {
+ var address = addressResolver.resolveSignalServiceAddress(recipientId);
+ syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
+ } catch (IOException e) {
+ logger.warn("Failed to send verification sync message: {}", e.getMessage());
+ }
+
+ return true;
+ }
+
+ public void handleIdentityFailure(
+ final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure
+ ) {
+ final var identityKey = identityFailure.getIdentityKey();
+ if (identityKey != null) {
+ final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
+ if (newIdentity) {
+ account.getSessionStore().archiveSessions(recipientId);
+ }
+ } else {
+ // Retrieve profile to get the current identity key from the server
+ profileHelper.refreshRecipientProfile(recipientId);
+ }
+ }
+}
private final SignalAccount account;
private final SignalDependencies dependencies;
private final AvatarStore avatarStore;
- private final ProfileKeyProvider profileKeyProvider;
private final UnidentifiedAccessProvider unidentifiedAccessProvider;
private final SignalServiceAddressResolver addressResolver;
final SignalAccount account,
final SignalDependencies dependencies,
final AvatarStore avatarStore,
- final ProfileKeyProvider profileKeyProvider,
final UnidentifiedAccessProvider unidentifiedAccessProvider,
final SignalServiceAddressResolver addressResolver
) {
this.account = account;
this.dependencies = dependencies;
this.avatarStore = avatarStore;
- this.profileKeyProvider = profileKeyProvider;
this.unidentifiedAccessProvider = unidentifiedAccessProvider;
this.addressResolver = addressResolver;
}
RecipientId recipientId, SignalServiceProfile.RequestType requestType
) {
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
- var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId));
+ var profileKey = Optional.fromNullable(account.getProfileStore().getProfileKey(recipientId));
final var address = addressResolver.resolveSignalServiceAddress(recipientId);
return retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
+++ /dev/null
-package org.asamk.signal.manager.helper;
-
-import org.asamk.signal.manager.storage.recipients.RecipientId;
-import org.signal.zkgroup.profiles.ProfileKey;
-
-public interface ProfileKeyProvider {
-
- ProfileKey getProfileKey(RecipientId address);
-}
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
) throws IOException, GroupSendingNotAllowedException {
GroupUtils.setGroupContext(messageBuilder, g);
- messageBuilder.withExpiration(g.getMessageExpirationTime());
+ messageBuilder.withExpiration(g.getMessageExpirationTimer());
final var message = messageBuilder.build();
final var recipients = g.getMembersWithout(account.getSelfRecipientId());
package org.asamk.signal.manager.helper;
+import org.asamk.signal.manager.SignalDependencies;
+import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
+import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
public class UnidentifiedAccessHelper {
- private final SelfProfileKeyProvider selfProfileKeyProvider;
-
- private final ProfileKeyProvider profileKeyProvider;
+ private final static Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
+ private final SignalAccount account;
+ private final SignalDependencies dependencies;
+ private final SelfProfileKeyProvider selfProfileKeyProvider;
private final ProfileProvider profileProvider;
- private final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider;
-
public UnidentifiedAccessHelper(
+ final SignalAccount account,
+ final SignalDependencies dependencies,
final SelfProfileKeyProvider selfProfileKeyProvider,
- final ProfileKeyProvider profileKeyProvider,
- final ProfileProvider profileProvider,
- final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider
+ final ProfileProvider profileProvider
) {
+ this.account = account;
+ this.dependencies = dependencies;
this.selfProfileKeyProvider = selfProfileKeyProvider;
- this.profileKeyProvider = profileKeyProvider;
this.profileProvider = profileProvider;
- this.senderCertificateProvider = senderCertificateProvider;
+ }
+
+ private byte[] getSenderCertificate() {
+ byte[] certificate;
+ try {
+ if (account.isPhoneNumberShared()) {
+ certificate = dependencies.getAccountManager().getSenderCertificate();
+ } else {
+ certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
+ }
+ } catch (IOException e) {
+ logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
+ return null;
+ }
+ // TODO cache for a day
+ return certificate;
}
private byte[] getSelfUnidentifiedAccessKey() {
switch (targetProfile.getUnidentifiedAccessMode()) {
case ENABLED:
- var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
+ var theirProfileKey = account.getProfileStore().getProfileKey(recipient);
if (theirProfileKey == null) {
return null;
}
public Optional<UnidentifiedAccessPair> getAccessForSync() {
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
- var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
+ var selfUnidentifiedAccessCertificate = getSenderCertificate();
if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
return Optional.absent();
public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) {
var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
- var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
+ var selfUnidentifiedAccessCertificate = getSenderCertificate();
if (recipientUnidentifiedAccessKey == null
|| selfUnidentifiedAccessKey == null
+++ /dev/null
-package org.asamk.signal.manager.helper;
-
-public interface UnidentifiedAccessSenderCertificateProvider {
-
- byte[] getSenderCertificate();
-}
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import java.util.Set;
public abstract void setBlocked(boolean blocked);
- public abstract int getMessageExpirationTime();
+ public abstract int getMessageExpirationTimer();
public abstract boolean isAnnouncementGroup();
+ public abstract GroupPermission getPermissionAddMember();
+
+ public abstract GroupPermission getPermissionEditDetails();
+
+ public abstract GroupPermission getPermissionSendMessage();
+
public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
}
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.recipients.RecipientId;
}
@Override
- public int getMessageExpirationTime() {
+ public int getMessageExpirationTimer() {
return messageExpirationTime;
}
return false;
}
+ @Override
+ public GroupPermission getPermissionAddMember() {
+ return GroupPermission.EVERY_MEMBER;
+ }
+
+ @Override
+ public GroupPermission getPermissionEditDetails() {
+ return GroupPermission.EVERY_MEMBER;
+ }
+
+ @Override
+ public GroupPermission getPermissionSendMessage() {
+ return GroupPermission.EVERY_MEMBER;
+ }
+
public void addMembers(Collection<RecipientId> members) {
this.members.addAll(members);
}
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.storageservice.protos.groups.AccessControl;
}
@Override
- public int getMessageExpirationTime() {
+ public int getMessageExpirationTimer() {
return this.group != null && this.group.hasDisappearingMessagesTimer()
? this.group.getDisappearingMessagesTimer().getDuration()
: 0;
return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED;
}
+ @Override
+ public GroupPermission getPermissionAddMember() {
+ final var accessControl = getAccessControl();
+ return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getMembers());
+ }
+
+ @Override
+ public GroupPermission getPermissionEditDetails() {
+ final var accessControl = getAccessControl();
+ return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getAttributes());
+ }
+
+ @Override
+ public GroupPermission getPermissionSendMessage() {
+ return isAnnouncementGroup() ? GroupPermission.ONLY_ADMINS : GroupPermission.EVERY_MEMBER;
+ }
+
public void setPermissionDenied(final boolean permissionDenied) {
this.permissionDenied = permissionDenied;
}
public boolean isPermissionDenied() {
return permissionDenied;
}
+
+ private AccessControl getAccessControl() {
+ if (this.group == null || !this.group.hasAccessControl()) {
+ return null;
+ }
+
+ return this.group.getAccessControl();
+ }
+
+ private static GroupPermission toGroupPermission(final AccessControl.AccessRequired permission) {
+ switch (permission) {
+ case ADMINISTRATOR:
+ return GroupPermission.ONLY_ADMINS;
+ case MEMBER:
+ default:
+ return GroupPermission.EVERY_MEMBER;
+ }
+ }
}
Where <type> is according to DBus specification:
-* <a> : Array of ... (comma-separated list, array:)
+* <a> : Array of ... (comma-separated list) (array:)
* (...) : Struct (cannot be sent via `dbus-send`)
* <b> : Boolean (false|true) (boolean:)
* <i> : Signed 32-bit (int) integer (int32:)
== Methods
=== Control methods
-These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`).
-Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to
-`/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_).
+These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`).
+Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to
+`/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_).
Only `version()` is activated in single-user mode; the rest are disabled.
link() -> deviceLinkUri<s>::
* newDeviceName : Name to give new device (defaults to "cli" if no name is given)
* deviceLinkUri : URI of newly linked device
-Returns a URI of the form "tsdevice:/?uuid=...". This can be piped to a QR encoder to create a display that
+Returns a URI of the form "sgnl://linkdevice/?uuid=...". This can be piped to a QR encoder to create a display that
can be captured by a Signal smartphone client. For example:
`dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client"|tr '\n' '\0'|sed 's/.*string //g'|sed 's/\"//g'|qrencode -s10 -tANSI256`
-Exception: Failure
+Exceptions: Failure
listAccounts() -> accountList<as>::
* accountList : Array of all attached accounts in DBus object path form
Command fails if PIN was set after previous registration; use verifyWithPin instead.
-Exception: Failure, InvalidNumber
+Exceptions: Failure, InvalidNumber
verifyWithPin(number<s>, verificationCode<s>, pin<s>) -> <>::
* number : Phone number
* verificationCode : Code received from Signal after successful registration request
* pin : PIN you set with setPin command after verifying previous registration
-Exception: Failure, InvalidNumber
+Exceptions: Failure, InvalidNumber
version() -> version<s>::
* version : Version string of signal-cli
Exceptions: None
-=== Other methods
+=== Group control methods
+The following methods listen to the recipient's object path, which is constructed as follows:
+"/org/asamk/Signal/" + DBusNumber
+* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+)
-updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
-* groupId : Byte array representing the internal group identifier
-* newName : New name of group (empty if unchanged)
-* members : String array of new members to be invited to group
-* avatar : Filename of avatar picture to be set for group (empty if none)
+createGroup(groupName<s>, members<as>, avatar<s>) -> groupId<ay>::
+* groupName : String representing the display name of the group
+* members : String array of new members to be invited to group
+* avatar : Filename of avatar picture to be set for group (empty if none)
+* groupId : Byte array representing the internal group identifier
-Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound
+Exceptions: AttachmentInvalid, Failure, InvalidNumber;
-updateProfile(name<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
-updateProfile(givenName<s>, familyName<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
-* name : Name for your own profile (empty if unchanged)
-* givenName : Given name for your own profile (empty if unchanged)
-* familyName : Family name for your own profile (empty if unchanged)
-* about : About message for profile (empty if unchanged)
-* aboutEmoji : Emoji for profile (empty if unchanged)
-* avatar : Filename of avatar picture for profile (empty if unchanged)
-* remove : Set to true if the existing avatar picture should be removed
+getGroup(groupId<ay>) -> objectPath<o>::
+* groupId : Byte array representing the internal group identifier
+* objectPath : DBusPath for the group
+
+getGroupMembers(groupId<ay>) -> members<as>::
+* groupId : Byte array representing the internal group identifier
+* members : String array with the phone numbers of all active members of a group
+
+Exceptions: None, if the group name is not found an empty array is returned
+
+joinGroup(inviteURI<s>) -> <>::
+* inviteURI : String starting with https://signal.group/#
+
+Behavior of this method depends on the `requirePermission` parameter of the `enableLink` method. If permission is required, `joinGroup` adds you to the requesting members list. Permission may be granted based on the group's `PermissionAddMember` property (`ONLY_ADMINS` or `EVERY_MEMBER`). If permission is not required, `joinGroup` admits you immediately to the group.
Exceptions: Failure
+listGroups() -> groups<a(oays)>::
+* groups : Array of Structs(objectPath, groupId, groupName)
+** objectPath : DBusPath
+** groupId : Byte array representing the internal group identifier
+** groupName : String representing the display name of the group
-setExpirationTimer(number<s>, expiration<i>) -> <>::
-* number : Phone number of recipient
-* expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration.
+sendGroupMessage(message<s>, attachments<as>, groupId<ay>) -> timestamp<x>::
+* message : Text to send (can be UTF8)
+* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
+* groupId : Byte array representing the internal group identifier
+* timestamp : Long, can be used to identify the corresponding Signal reply
+
+Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId
+
+sendGroupMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
+* emoji : Unicode grapheme cluster of the emoji
+* remove : Boolean, whether a previously sent reaction (emoji) should be removed
+* targetAuthor : String with the phone number of the author of the message to which to react
+* targetSentTimestamp : Long representing timestamp of the message to which to react
+* groupId : Byte array representing the internal group identifier
+* timestamp : Long, can be used to identify the corresponding signal reply
+
+Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId
+
+sendGroupRemoteDeleteMessage(targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
+* targetSentTimestamp : Long representing timestamp of the message to delete
+* groupId : Byte array with base64 encoded group identifier
+* timestamp : Long, can be used to identify the corresponding signal reply
+
+Exceptions: Failure, GroupNotFound, InvalidGroupId
+
+=== Group methods
+The following methods listen to the group's object path, which can be obtained from the listGroups() method and is constructed as follows:
+"/org/asamk/Signal/" + DBusNumber + "/Groups/" + DBusGroupId
+* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+)
+* DBusGroupId : groupId in base64 format, with underscore (_) replacing plus (+), equals (=), or slash (/)
+
+Groups have the following (case-sensitive) properties:
+* Id<ay> (read-only) : Byte array representing the internal group identifier
+* Name<s> : Display name of the group
+* Description<s> : Description of the group
+* Avatar<s> (write-only) : Filename of the avatar
+* IsBlocked<b> : true=member will not receive group messages; false=not blocked
+* IsMember<b> (read-only) : always true (object path exists only for group members)
+* IsAdmin<b> (read-only) : true=member has admin privileges; false=not admin
+* MessageExpirationTimer<i> : int32 representing message expiration time for group
+* Members<as> (read-only) : String array of group members' phone numbers
+* PendingMembers<as> (read-only) : String array of pending members' phone numbers
+* RequestingMembers<as> (read-only) : String array of requesting members' phone numbers
+* Admins<as> (read-only) : String array of admins' phone numbers
+* PermissionAddMember<s> : String representing who has permission to add members
+** ONLY_ADMINS, EVERY_MEMBER
+* PermissionEditDetails<s> : String representing who may edit group details
+** ONLY_ADMINS, EVERY_MEMBER
+* PermissionSendMessage<s> : String representing who post messages to group
+** ONLY_ADMINS, EVERY_MEMBER (note that ONLY_ADMINS is equivalent to IsAnnouncementGroup)
+* GroupInviteLink<s> (read-only) : String of the invitation link (starts with https://signal.group/#)
+
+To get a property, use (replacing `--session` with `--system` if needed):
+`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Group string:$PROPERTY_NAME`
+
+To set a property, use:
+`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Group string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE`
+
+To get all properties, use:
+`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Group`
+
+addAdmins(recipients<as>) -> <>::
+* recipients : String array of phone numbers
+
+Grant admin privileges to recipients.
Exceptions: Failure
-setContactBlocked(number<s>, block<b>) -> <>::
-* number : Phone number affected by method
-* block : 0=remove block , 1=blocked
+addMembers(recipients<as>) -> <>::
+* recipients : String array of phone numbers
-Messages from blocked numbers will no longer be forwarded via DBus.
+Add recipients to group if they are pending members; otherwise add recipients to list of requesting members.
-Exceptions: InvalidNumber
+Exceptions: Failure
-setGroupBlocked(groupId<ay>, block<b>) -> <>::
-* groupId : Byte array representing the internal group identifier
-* block : 0=remove block , 1=blocked
+disableLink() -> <>::
-Messages from blocked groups will no longer be forwarded via DBus.
+Disables the group's invitation link.
+
+Exceptions: Failure
-Exceptions: GroupNotFound
+enableLink(requiresApproval<b>) -> <>::
+* requiresApproval : true=add numbers using the link to the requesting members list
-joinGroup(inviteURI<s>) -> <>::
-* inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App
+Enables the group's invitation link.
+
+Exceptions: Failure
+
+quitGroup() -> <>::
+Exceptions: Failure, LastGroupAdmin
+
+removeAdmins(recipients<as>) -> <>::
+* recipients : String array of phone numbers
+
+Remove admin privileges from recipients.
Exceptions: Failure
+removeMembers(recipients<as>) -> <>::
+* recipients : String array of phone numbers
+
+Remove recipients from group.
+
+Exceptions: Failure
+
+resetLink() -> <>::
+
+Resets the group's invitation link to a new random URL starting with https://signal.group/#
+
+Exceptions: Failure
+
+=== Deprecated group control methods
+The following deprecated methods listen to the recipient's object path, which is constructed as follows:
+"/org/asamk/Signal/" + DBusNumber
+* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+)
+
+getGroupIds() -> groupList<aay>::
+groupList : Array of Byte arrays representing the internal group identifiers
+
+All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked()
+
+Exceptions: None
+
+getGroupName(groupId<ay>) -> groupName<s>::
+* groupId : Byte array representing the internal group identifier
+* groupName : The display name of the group
+
+Exceptions: None, if the group name is not found an empty string is returned
+
+isGroupBlocked(groupId<ay>) -> isGroupBlocked<b>::
+* groupId : Byte array representing the internal group identifier
+* isGroupBlocked : true=group is blocked; false=group is not blocked
+
+Dbus will not forward messages from a group when you have blocked it.
+
+Exceptions: InvalidGroupId, Failure
+
+isMember(groupId<ay>) -> isMember<b>::
+* groupId : Byte array representing the internal group identifier
+* isMember : true=you are a group member; false=you are not a group member
+
+Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false)
+
quitGroup(groupId<ay>) -> <>::
* groupId : Byte array representing the internal group identifier
Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember()
-Exceptions: GroupNotFound, Failure
+Exceptions: GroupNotFound, Failure, InvalidGroupId
-isMember(groupId<ay>) -> active<b>::
+setGroupBlocked(groupId<ay>, block<b>) -> <>::
* groupId : Byte array representing the internal group identifier
+* block : false=remove block , true=blocked
-Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false)
+Messages from blocked groups will no longer be forwarded via DBus.
-sendEndSessionMessage(recipients<as>) -> <>::
-* recipients : Array of phone numbers
+Exceptions: GroupNotFound, InvalidGroupId
-Exceptions: Failure, InvalidNumber, UntrustedIdentity
+updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
+* groupId : Byte array representing the internal group identifier
+* newName : New name of group (empty if unchanged)
+* members : String array of new members to be invited to group
+* avatar : Filename of avatar picture to be set for group (empty if none)
-sendGroupMessage(message<s>, attachments<as>, groupId<ay>) -> timestamp<x>::
-* message : Text to send (can be UTF8)
-* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
-* groupId : Byte array representing the internal group identifier
-* timestamp : Can be used to identify the corresponding signal reply
+Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound
+
+=== Device control methods
+The following methods listen to the recipient's object path, which is constructed as follows:
+"/org/asamk/Signal/" + DBusNumber
+* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+)
+
+addDevice(deviceUri<s>) -> <>::
+* deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method.
+
+getDevice(deviceId<x>) -> devicePath<o>::
+* deviceId : Long representing a deviceId
+* devicePath : DBusPath object for the device
+
+Exceptions: DeviceNotFound
+
+listDevices() -> devices<a(oxs)>::
+* devices : Array of structs (objectPath, id, name)
+** objectPath : DBusPath representing the device's object path
+** id : Long representing the deviceId
+** name : String representing the device's name
-Exceptions: GroupNotFound, Failure, AttachmentInvalid
+Exceptions: InvalidUri
sendContacts() -> <>::
Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device.
-Exception: Failure
+Exceptions: Failure
-sendNoteToSelfMessage(message<s>, attachments<as>) -> timestamp<x>::
-* message : Text to send (can be UTF8)
-* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
-* timestamp : Can be used to identify the corresponding signal reply
+=== Device methods and properties
+The following methods listen to the device's object path, which is constructed as follows:
+"/org/asamk/Signal/" + DBusNumber + "/Devices/" + deviceId
+* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+)
+* deviceId : Long representing the device identifier (obtained from listDevices() method)
-Exceptions: Failure, AttachmentInvalid
+Devices have the following (case-sensitive) properties:
+* Id<x> (read-only) : Long representing the device identifier
+* Created<x> (read-only) : Long representing the number of milliseconds since the Unix epoch
+* LastSeen<x> (read-only) : Long representing the number of milliseconds since the Unix epoch
+* Name<s> : String representing the display name of the device
-sendMessage(message<s>, attachments<as>, recipient<s>) -> timestamp<x>::
-sendMessage(message<s>, attachments<as>, recipients<as>) -> timestamp<x>::
-* message : Text to send (can be UTF8)
-* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
-* recipient : Phone number of a single recipient
-* recipients : Array of phone numbers
-* timestamp : Can be used to identify the corresponding signal reply
+To get a property, use (replacing `--session` with `--system` if needed):
+`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Device string:$PROPERTY_NAME`
-Depending on the type of the recipient field this sends a message to one or multiple recipients.
+To set a property, use:
+`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Device string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE`
-Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity
+To get all properties, use:
+`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Device`
-sendTyping(recipient<s>, stop<b>) -> <>::
-* recipient : Phone number of a single recipient
-* targetSentTimestamp : True, if typing state should be stopped
+removeDevice() -> <>::
-Exceptions: Failure, GroupNotFound, UntrustedIdentity
+Exceptions: Failure
+=== Other methods
-sendReadReceipt(recipient<s>, targetSentTimestamp<ax>) -> <>::
-* recipient : Phone number of a single recipient
-* targetSentTimestamp : Array of Longs to identify the corresponding signal messages
+getContactName(number<s>) -> name<s>::
+* number : Phone number
+* name : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used
-Exceptions: Failure, UntrustedIdentity
+Exceptions: None
-sendGroupMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
-* emoji : Unicode grapheme cluster of the emoji
-* remove : Boolean, whether a previously sent reaction (emoji) should be removed
-* targetAuthor : String with the phone number of the author of the message to which to react
-* targetSentTimestamp : Long representing timestamp of the message to which to react
-* groupId : Byte array with base64 encoded group identifier
-* timestamp : Long, can be used to identify the corresponding signal reply
+getContactNumber(name<s>) -> numbers<as>::
+* numbers : Array of phone number
+* name : Contact or profile name ("firstname lastname")
+
+Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set.
+
+Exceptions: None
+
+getSelfNumber() -> number<s>::
+* number : Your phone number
+
+Exceptions: None
+
+isContactBlocked(number<s>) -> blocked<b>::
+* number : Phone number
+* blocked : true=blocked, false=not blocked
+
+For unknown numbers false is returned but no exception is raised.
+
+Exceptions: InvalidPhoneNumber
+
+isRegistered() -> result<b>::
+isRegistered(number<s>) -> result<b>::
+isRegistered(numbers<as>) -> results<ab>::
+* number : Phone number
+* numbers : String array of phone numbers
+* result : true=number is registered, false=number is not registered
+* results : Boolean array of results
+
+For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered).
+
+Exceptions: InvalidNumber
+
+listNumbers() -> numbers<as>::
+* numbers : String array of all known numbers
+
+This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages)
+
+Exceptions: None
+
+removePin() -> <>::
+
+Removes registration PIN protection.
+
+Exceptions: Failure
+
+sendEndSessionMessage(recipients<as>) -> <>::
+* recipients : Array of phone numbers
+
+Exceptions: Failure, InvalidNumber, UntrustedIdentity
+
+sendMessage(message<s>, attachments<as>, recipient<s>) -> timestamp<x>::
+sendMessage(message<s>, attachments<as>, recipients<as>) -> timestamp<x>::
+* message : Text to send (can be UTF8)
+* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
+* recipient : Phone number of a single recipient
+* recipients : String array of phone numbers
+* timestamp : Long, can be used to identify the corresponding Signal reply
-Exceptions: Failure, InvalidNumber, GroupNotFound
+Depending on the type of the recipient field this sends a message to one or multiple recipients.
+
+Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity
sendMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, recipient<s>) -> timestamp<x>::
sendMessageReaction(emoji<s>, remove<b>, targetAuthor<s>, targetSentTimestamp<x>, recipients<as>) -> timestamp<x>::
* targetSentTimestamp : Long representing timestamp of the message to which to react
* recipient : String with the phone number of a single recipient
* recipients : Array of strings with phone numbers, should there be more recipients
-* timestamp : Long, can be used to identify the corresponding signal reply
+* timestamp : Long, can be used to identify the corresponding Signal reply
Depending on the type of the recipient(s) field this sends a reaction to one or multiple recipients.
Exceptions: Failure, InvalidNumber
-sendGroupRemoteDeleteMessage(targetSentTimestamp<x>, groupId<ay>) -> timestamp<x>::
-* targetSentTimestamp : Long representing timestamp of the message to delete
-* groupId : Byte array with base64 encoded group identifier
-* timestamp : Long, can be used to identify the corresponding signal reply
+sendNoteToSelfMessage(message<s>, attachments<as>) -> timestamp<x>::
+* message : Text to send (can be UTF8)
+* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
+* timestamp : Long, can be used to identify the corresponding Signal reply
-Exceptions: Failure, GroupNotFound
+Exceptions: Failure, AttachmentInvalid
+
+sendReadReceipt(recipient<s>, targetSentTimestamps<ax>) -> <>::
+* recipient : Phone number of a single recipient
+* targetSentTimestamps : Array of Longs to identify the corresponding Signal messages
+
+Exceptions: Failure, UntrustedIdentity
sendRemoteDeleteMessage(targetSentTimestamp<x>, recipient<s>) -> timestamp<x>::
sendRemoteDeleteMessage(targetSentTimestamp<x>, recipients<as>) -> timestamp<x>::
Exceptions: Failure, InvalidNumber
-getContactName(number<s>) -> name<s>::
-* number : Phone number
-* name : Contact's name in local storage (from the primary device for a linked account, or the one set with setContactName); if not set, contact's profile name is used
-
-setContactName(number<s>,name<>) -> <>::
-* number : Phone number
-* name : Name to be set in contacts (in local storage with signal-cli)
-
-getGroupIds() -> groupList<aay>::
-groupList : Array of Byte arrays representing the internal group identifiers
-
-All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked()
-
-getGroupName(groupId<ay>) -> groupName<s>::
-groupName : The display name of the group
-groupId : Byte array representing the internal group identifier
-
-Exceptions: None, if the group name is not found an empty string is returned
-
-getGroupMembers(groupId<ay>) -> members<as>::
-members : String array with the phone numbers of all active members of a group
-groupId : Byte array representing the internal group identifier
-
-Exceptions: None, if the group name is not found an empty array is returned
+sendTyping(recipient<s>, stop<b>) -> <>::
+* recipient : Phone number of a single recipient
+* targetSentTimestamp : True, if typing state should be stopped
-listNumbers() -> numbers<as>::
-numbers : String array of all known numbers
+Exceptions: Failure, GroupNotFound, UntrustedIdentity
-This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages)
+setContactBlocked(number<s>, block<b>) -> <>::
+* number : Phone number affected by method
+* block : false=remove block, true=blocked
-getContactNumber(name<s>) -> numbers<as>::
-* numbers : Array of phone number
-* name : Contact or profile name ("firstname lastname")
+Messages from blocked numbers will no longer be forwarded via DBus.
-Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set.
+Exceptions: InvalidNumber
-isContactBlocked(number<s>) -> state<b>::
+setContactName(number<s>,name<>) -> <>::
* number : Phone number
-* state : 1=blocked, 0=not blocked
-
-Exceptions: None, for unknown numbers 0 (false) is returned
-
-isGroupBlocked(groupId<ay>) -> state<b>::
-* groupId : Byte array representing the internal group identifier
-* state : 1=blocked, 0=not blocked
-
-Exceptions: None, for unknown groups 0 (false) is returned
+* name : Name to be set in contacts (in local storage with signal-cli)
-removePin() -> <>::
+Exceptions: InvalidNumber, Failure
-Removes registration PIN protection.
+setExpirationTimer(number<s>, expiration<i>) -> <>::
+* number : Phone number of recipient
+* expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration.
-Exception: Failure
+Exceptions: Failure, InvalidNumber
setPin(pin<s>) -> <>::
* pin : PIN you set after registration (resets after 7 days of inactivity)
Sets a registration lock PIN, to prevent others from registering your number.
-Exception: Failure
-
-version() -> version<s>::
-* version : Version string of signal-cli
-
-isRegistered() -> result<b>::
-isRegistered(number<s>) -> result<b>::
-isRegistered(numbers<as>) -> results<ab>::
-* number : Phone number
-* numbers : String array of phone numbers
-* result : true=number is registered, false=number is not registered
-* results : Boolean array of results
-
-Exception: InvalidNumber for an incorrectly formatted phone number. For unknown numbers, false is returned, but no exception is raised. If no number is given, returns whether you are registered (presumably true).
-
-addDevice(deviceUri<s>) -> <>::
-* deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app
-
-Exception: InvalidUri
-
-listDevices() -> devices<a(oxs)>::
-* devices : Array of structs (objectPath, id, name)
-** objectPath : DBusPath representing the device's object path
-** id : Long representing the deviceId
-** name : String representing the device's name
-
-Exception: Failure
-
-removeDevice(deviceId<i>) -> <>::
-* deviceId : Device ID to remove, obtained from listDevices() command
+Exceptions: Failure
-Exception: Failure
+submitRateLimitChallenge(challenge<s>, captcha<s>) -> <>::
+* challenge : The challenge token taken from the proof required error.
+* captcha : The captcha token from the solved captcha on the Signal website..
+Can be used to lift some rate-limits by solving a captcha.
-updateDeviceName(deviceName<s>) -> <>::
-* deviceName : New name
+Exception: IOErrorException
-Set a new name for this device (main or linked).
+updateProfile(name<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
+updateProfile(givenName<s>, familyName<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
+* name : Name for your own profile (empty if unchanged)
+* givenName : Given name for your own profile (empty if unchanged)
+* familyName : Family name for your own profile (empty if unchanged)
+* about : About message for profile (empty if unchanged)
+* aboutEmoji : Emoji for profile (empty if unchanged)
+* avatar : Filename of avatar picture for profile (empty if unchanged)
+* remove : Set to true if the existing avatar picture should be removed
-Exception: Failure
+Exceptions: Failure
uploadStickerPack(stickerPackPath<s>) -> url<s>::
* stickerPackPath : Path to the manifest.json file or a zip file in the same directory
Exceptions: IOError, UserError
-submitRateLimitChallenge(challenge<s>, captcha<s>) -> <>::
-* challenge : The challenge token taken from the proof required error.
-* captcha : The captcha token from the solved captcha on the Signal website..
-Can be used to lift some rate-limits by solving a captcha.
+version() -> version<s>::
+* version : Version string of signal-cli
-Exception: IOErrorException
+Exceptions: None
== Signals
-SyncMessageReceived (timestamp<x>, sender<s>, destination<s>, groupId<ay>,message<s>, attachments<as>)::
+SyncMessageReceived (timestamp<x>, sender<s>, destination<s>, groupId<ay>, message<s>, attachments<as>)::
+* timestamp : Integer value that can be used to associate this e.g. with a sendMessage()
+* sender : Phone number of the sender
+* destination : DBus code for destination
+* groupId : Byte array representing the internal group identifier (empty when private message)
+* message : Message text
+* attachments : String array of filenames in the signal-cli storage (~/.local/share/signal-cli/attachments/)
+
The sync message is received when the user sends a message from a linked device.
ReceiptReceived (timestamp<x>, sender<s>)::
* sender : Phone number of the sender
* groupId : Byte array representing the internal group identifier (empty when private message)
* message : Message text
-* attachments : String array of filenames for the attachments. These files are located in the signal-cli storage and the current user needs to have read access there
+* attachments : String array of filenames in the signal-cli storage (~/.local/share/signal-cli/attachments/)
This signal is received whenever we get a private message or a message is posted in a group we are an active member
=== link
Link to an existing device, instead of registering a new number.
-This shows a "tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can just use this URI.
+This shows a "sgnl://linkdevice/?uuid=..." URI. If you want to connect to another signal-cli instance, you can just use this URI.
If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app.
*-n* NAME, *--name* NAME::
*--uri* URI::
Specify the uri contained in the QR code shown by the new device.
-You will need the full uri enclosed in quotation marks, such as "tsdevice:/?uuid=....."
+You will need the full URI such as "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...")
+Make sure to enclose it in quotation marks for shells.
=== listDevices
package org.asamk;
import org.asamk.signal.commands.exceptions.IOErrorException;
-
import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.Struct;
import org.freedesktop.dbus.annotations.DBusProperty;
void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber;
+ @Deprecated
void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId;
+ @Deprecated
List<byte[]> getGroupIds();
+ DBusPath getGroup(byte[] groupId);
+
+ List<StructGroup> listGroups();
+
+ @Deprecated
String getGroupName(byte[] groupId) throws Error.InvalidGroupId;
+ @Deprecated
List<String> getGroupMembers(byte[] groupId) throws Error.InvalidGroupId;
+ byte[] createGroup(
+ String name, List<String> members, String avatar
+ ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
+
+ @Deprecated
byte[] updateGroup(
byte[] groupId, String name, List<String> members, String avatar
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId;
List<String> getContactNumber(final String name) throws Error.Failure;
+ @Deprecated
void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId;
boolean isContactBlocked(final String number) throws Error.InvalidNumber;
+ @Deprecated
boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId;
+ @Deprecated
boolean isMember(final byte[] groupId) throws Error.InvalidGroupId;
byte[] joinGroup(final String groupLink) throws Error.Failure;
void removeDevice() throws Error.Failure;
}
+ class StructGroup extends Struct {
+
+ @Position(0)
+ DBusPath objectPath;
+
+ @Position(1)
+ byte[] id;
+
+ @Position(2)
+ String name;
+
+ public StructGroup(final DBusPath objectPath, final byte[] id, final String name) {
+ this.objectPath = objectPath;
+ this.id = id;
+ this.name = name;
+ }
+
+ public DBusPath getObjectPath() {
+ return objectPath;
+ }
+
+ public byte[] getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ @DBusProperty(name = "Id", type = Byte[].class, access = DBusProperty.Access.READ)
+ @DBusProperty(name = "Name", type = String.class)
+ @DBusProperty(name = "Description", type = String.class)
+ @DBusProperty(name = "Avatar", type = String.class, access = DBusProperty.Access.WRITE)
+ @DBusProperty(name = "IsBlocked", type = Boolean.class)
+ @DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ)
+ @DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ)
+ @DBusProperty(name = "MessageExpirationTimer", type = Integer.class)
+ @DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ)
+ @DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ)
+ @DBusProperty(name = "RequestingMembers", type = String[].class, access = DBusProperty.Access.READ)
+ @DBusProperty(name = "Admins", type = String[].class, access = DBusProperty.Access.READ)
+ @DBusProperty(name = "PermissionAddMember", type = String.class)
+ @DBusProperty(name = "PermissionEditDetails", type = String.class)
+ @DBusProperty(name = "PermissionSendMessage", type = String.class)
+ @DBusProperty(name = "GroupInviteLink", type = String.class, access = DBusProperty.Access.READ)
+ interface Group extends DBusInterface, Properties {
+
+ void quitGroup() throws Error.Failure, Error.LastGroupAdmin;
+
+ void addMembers(List<String> recipients) throws Error.Failure;
+
+ void removeMembers(List<String> recipients) throws Error.Failure;
+
+ void addAdmins(List<String> recipients) throws Error.Failure;
+
+ void removeAdmins(List<String> recipients) throws Error.Failure;
+
+ void resetLink() throws Error.Failure;
+
+ void disableLink() throws Error.Failure;
+
+ void enableLink(boolean requiresApproval) throws Error.Failure;
+ }
+
interface Error {
class AttachmentInvalid extends DBusExecutionException {
}
}
+ class DeviceNotFound extends DBusExecutionException {
+
+ public DeviceNotFound(final String message) {
+ super(message);
+ }
+ }
+
class GroupNotFound extends DBusExecutionException {
public GroupNotFound(final String message) {
}
}
+ class LastGroupAdmin extends DBusExecutionException {
+
+ public LastGroupAdmin(final String message) {
+ super(message);
+ }
+ }
+
class InvalidNumber extends DBusExecutionException {
public InvalidNumber(final String message) {
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.util.DateUtils;
-import org.asamk.signal.util.Util;
import org.slf4j.helpers.MessageFormatter;
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
writer.println("Received sync message with verified identities:");
final var verifiedMessage = syncMessage.getVerified().get();
writer.println("- {}: {}", formatContact(verifiedMessage.getDestination()), verifiedMessage.getVerified());
- var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(),
- verifiedMessage.getIdentityKey()));
- writer.indentedWriter().println(safetyNumber);
}
if (syncMessage.getConfiguration().isPresent()) {
writer.println("Received sync message with configuration:");
for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) {
try {
m.setGroupBlocked(groupId, true);
+ } catch (NotMasterDeviceException e) {
+ throw new UserErrorException("This command doesn't work on linked devices.");
} catch (GroupNotFoundException e) {
logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage());
} catch (IOException e) {
resolveMembers(group.getPendingMembers()),
resolveMembers(group.getRequestingMembers()),
resolveMembers(group.getAdminMembers()),
- group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s",
+ group.getMessageExpirationTimer() == 0 ? "disabled" : group.getMessageExpirationTimer() + "s",
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
} else {
writer.println("Id: {} Name: {} Active: {} Blocked: {}",
group.getDescription(),
group.isMember(),
group.isBlocked(),
- group.getMessageExpirationTime(),
+ group.getMessageExpirationTimer(),
resolveJsonMembers(group.getMembers()),
resolveJsonMembers(group.getPendingMembers()),
resolveJsonMembers(group.getRequestingMembers()),
resolveJsonMembers(group.getAdminMembers()),
+ group.getPermissionAddMember().name(),
+ group.getPermissionEditDetails().name(),
+ group.getPermissionSendMessage().name(),
groupInviteLink == null ? null : groupInviteLink.getUrl());
}).collect(Collectors.toList());
public final Set<JsonGroupMember> pendingMembers;
public final Set<JsonGroupMember> requestingMembers;
public final Set<JsonGroupMember> admins;
+ public final String permissionAddMember;
+ public final String permissionEditDetails;
+ public final String permissionSendMessage;
public final String groupInviteLink;
public JsonGroup(
Set<JsonGroupMember> pendingMembers,
Set<JsonGroupMember> requestingMembers,
Set<JsonGroupMember> admins,
+ final String permissionAddMember,
+ final String permissionEditDetails,
+ final String permissionSendMessage,
String groupInviteLink
) {
this.id = id;
this.pendingMembers = pendingMembers;
this.requestingMembers = requestingMembers;
this.admins = admins;
+ this.permissionAddMember = permissionAddMember;
+ this.permissionEditDetails = permissionEditDetails;
+ this.permissionSendMessage = permissionSendMessage;
this.groupInviteLink = groupInviteLink;
}
}
for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) {
try {
m.setGroupBlocked(groupId, false);
+ } catch (NotMasterDeviceException e) {
+ throw new UserErrorException("This command doesn't work on linked devices.");
} catch (GroupNotFoundException e) {
logger.warn("Unknown group id: {}", groupId);
} catch (IOException e) {
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupNotFoundException;
}
var results = m.updateGroup(groupId,
- groupName,
- groupDescription,
- groupMembers,
- groupRemoveMembers,
- groupAdmins,
- groupRemoveAdmins,
- groupResetLink,
- groupLinkState,
- groupAddMemberPermission,
- groupEditDetailsPermission,
- groupAvatar == null ? null : new File(groupAvatar),
- groupExpiration,
- groupSendMessagesPermission == null
- ? null
- : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS);
+ UpdateGroup.newBuilder()
+ .withName(groupName)
+ .withDescription(groupDescription)
+ .withMembers(groupMembers)
+ .withRemoveMembers(groupRemoveMembers)
+ .withAdmins(groupAdmins)
+ .withRemoveAdmins(groupRemoveAdmins)
+ .withResetGroupLink(groupResetLink)
+ .withGroupLinkState(groupLinkState)
+ .withAddMemberPermission(groupAddMemberPermission)
+ .withEditDetailsPermission(groupEditDetailsPermission)
+ .withAvatarFile(groupAvatar == null ? null : new File(groupAvatar))
+ .withExpirationTimer(groupExpiration)
+ .withIsAnnouncementGroup(groupSendMessagesPermission == null
+ ? null
+ : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS)
+ .build());
if (results != null) {
timestamp = results.getTimestamp();
ErrorUtils.handleSendMessageResults(results.getResults());
import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
-import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.interfaces.DBusInterface;
-import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
@Override
public List<Group> getGroups() {
- final var groupIds = signal.getGroupIds();
- return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList());
+ final var groups = signal.listGroups();
+ return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList());
}
@Override
if (groupAdmins.size() > 0) {
throw new UnsupportedOperationException();
}
- signal.quitGroup(groupId.serialize());
+ final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
+ group.quitGroup();
return new SendGroupMessageResults(0, List.of());
}
public Pair<GroupId, SendGroupMessageResults> createGroup(
final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile
) throws IOException, AttachmentInvalidException {
- final var newGroupId = signal.updateGroup(new byte[0],
- emptyIfNull(name),
+ final var newGroupId = signal.createGroup(emptyIfNull(name),
members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
avatarFile == null ? "" : avatarFile.getPath());
return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
@Override
public SendGroupMessageResults updateGroup(
- final GroupId groupId,
- final String name,
- final String description,
- final Set<RecipientIdentifier.Single> members,
- final Set<RecipientIdentifier.Single> removeMembers,
- final Set<RecipientIdentifier.Single> admins,
- final Set<RecipientIdentifier.Single> removeAdmins,
- final boolean resetGroupLink,
- final GroupLinkState groupLinkState,
- final GroupPermission addMemberPermission,
- final GroupPermission editDetailsPermission,
- final File avatarFile,
- final Integer expirationTimer,
- final Boolean isAnnouncementGroup
+ final GroupId groupId, final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
- signal.updateGroup(groupId.serialize(),
- emptyIfNull(name),
- members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
- avatarFile == null ? "" : avatarFile.getPath());
+ final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
+ if (updateGroup.getName() != null) {
+ group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
+ }
+ if (updateGroup.getDescription() != null) {
+ group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription());
+ }
+ if (updateGroup.getAvatarFile() != null) {
+ group.Set("org.asamk.Signal.Group",
+ "Avatar",
+ updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath());
+ }
+ if (updateGroup.getExpirationTimer() != null) {
+ group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer());
+ }
+ if (updateGroup.getAddMemberPermission() != null) {
+ group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name());
+ }
+ if (updateGroup.getEditDetailsPermission() != null) {
+ group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name());
+ }
+ if (updateGroup.getIsAnnouncementGroup() != null) {
+ group.Set("org.asamk.Signal.Group",
+ "PermissionSendMessage",
+ updateGroup.getIsAnnouncementGroup()
+ ? GroupPermission.ONLY_ADMINS.name()
+ : GroupPermission.EVERY_MEMBER.name());
+ }
+ if (updateGroup.getMembers() != null) {
+ group.addMembers(updateGroup.getMembers()
+ .stream()
+ .map(RecipientIdentifier.Single::getIdentifier)
+ .collect(Collectors.toList()));
+ }
+ if (updateGroup.getRemoveMembers() != null) {
+ group.removeMembers(updateGroup.getRemoveMembers()
+ .stream()
+ .map(RecipientIdentifier.Single::getIdentifier)
+ .collect(Collectors.toList()));
+ }
+ if (updateGroup.getAdmins() != null) {
+ group.addAdmins(updateGroup.getAdmins()
+ .stream()
+ .map(RecipientIdentifier.Single::getIdentifier)
+ .collect(Collectors.toList()));
+ }
+ if (updateGroup.getRemoveAdmins() != null) {
+ group.removeAdmins(updateGroup.getRemoveAdmins()
+ .stream()
+ .map(RecipientIdentifier.Single::getIdentifier)
+ .collect(Collectors.toList()));
+ }
+ if (updateGroup.isResetGroupLink()) {
+ group.resetLink();
+ }
+ if (updateGroup.getGroupLinkState() != null) {
+ switch (updateGroup.getGroupLinkState()) {
+ case DISABLED:
+ group.disableLink();
+ break;
+ case ENABLED:
+ group.enableLink(false);
+ break;
+ case ENABLED_WITH_APPROVAL:
+ group.enableLink(true);
+ break;
+ }
+ }
return new SendGroupMessageResults(0, List.of());
}
public void setGroupBlocked(
final GroupId groupId, final boolean blocked
) throws GroupNotFoundException, IOException {
- signal.setGroupBlocked(groupId.serialize(), blocked);
+ setGroupProperty(groupId, "IsBlocked", blocked);
+ }
+
+ private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
+ final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
+ group.Set("org.asamk.Signal.Group", propertyName, blocked);
}
@Override
@Override
public Group getGroup(final GroupId groupId) {
- final var id = groupId.serialize();
- return new Group(groupId,
- signal.getGroupName(id),
- null,
- null,
- signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()),
- Set.of(),
- Set.of(),
- Set.of(),
- signal.isGroupBlocked(id),
- 0,
- false,
- signal.isMember(id));
+ final var groupPath = signal.getGroup(groupId.serialize());
+ return getGroup(groupPath);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Group getGroup(final DBusPath groupPath) {
+ final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
+ final var id = (byte[]) group.get("Id").getValue();
+ try {
+ 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(null, m))
+ .collect(Collectors.toSet()),
+ ((List<String>) group.get("PendingMembers").getValue()).stream()
+ .map(m -> new RecipientAddress(null, m))
+ .collect(Collectors.toSet()),
+ ((List<String>) group.get("RequestingMembers").getValue()).stream()
+ .map(m -> new RecipientAddress(null, m))
+ .collect(Collectors.toSet()),
+ ((List<String>) group.get("Admins").getValue()).stream()
+ .map(m -> new RecipientAddress(null, m))
+ .collect(Collectors.toSet()),
+ (boolean) group.get("IsBlocked").getValue(),
+ (int) group.get("MessageExpirationTimer").getValue(),
+ GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
+ GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
+ GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
+ (boolean) group.get("IsMember").getValue(),
+ (boolean) group.get("IsAdmin").getValue());
+ } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
+ throw new AssertionError(e);
+ }
}
@Override
throw new UnsupportedOperationException();
}
- @Override
- public String computeSafetyNumber(
- final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
- ) {
- throw new UnsupportedOperationException();
- }
-
@Override
public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
return address;
this.setter = null;
}
+ public DbusProperty(final String name, final Consumer<T> setter) {
+ this.name = name;
+ this.getter = null;
+ this.setter = setter;
+ }
+
public String getName() {
return name;
}
package org.asamk.signal.dbus;
import org.asamk.Signal;
-import org.asamk.Signal.Error;
import org.asamk.signal.BaseConfig;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
+import org.freedesktop.dbus.types.Variant;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
private DBusPath thisDevice;
private final List<StructDevice> devices = new ArrayList<>();
+ private final List<StructGroup> groups = new ArrayList<>();
public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) {
this.m = m;
public void initObjects() {
updateDevices();
+ updateGroups();
}
public void close() {
@Override
public DBusPath getDevice(long deviceId) {
updateDevices();
- return new DBusPath(getDeviceObjectPath(objectPath, deviceId));
+ final var deviceOptional = devices.stream().filter(g -> g.getId().equals(deviceId)).findFirst();
+ if (deviceOptional.isEmpty()) {
+ throw new Error.DeviceNotFound("Device not found");
+ }
+ return deviceOptional.get().getObjectPath();
}
@Override
public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
try {
m.setGroupBlocked(getGroupId(groupId), blocked);
+ } catch (NotMasterDeviceException e) {
+ throw new Error.Failure("This command doesn't work on linked devices.");
} catch (GroupNotFoundException e) {
throw new Error.GroupNotFound(e.getMessage());
} catch (IOException e) {
return ids;
}
+ @Override
+ public DBusPath getGroup(final byte[] groupId) {
+ updateGroups();
+ final var groupOptional = groups.stream().filter(g -> Arrays.equals(g.getId(), groupId)).findFirst();
+ if (groupOptional.isEmpty()) {
+ throw new Error.GroupNotFound("Group not found");
+ }
+ return groupOptional.get().getObjectPath();
+ }
+
+ @Override
+ public List<StructGroup> listGroups() {
+ updateGroups();
+ return groups;
+ }
+
@Override
public String getGroupName(final byte[] groupId) {
var group = m.getGroup(getGroupId(groupId));
if (group == null) {
return List.of();
} else {
- return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
+ final var members = group.getMembers();
+ return getRecipientStrings(members);
}
}
+ @Override
+ public byte[] createGroup(
+ final String name, final List<String> members, final String avatar
+ ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber {
+ return updateGroup(new byte[0], name, members, avatar);
+ }
+
@Override
public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
try {
return results.first().serialize();
} else {
final var results = m.updateGroup(getGroupId(groupId),
- name,
- null,
- memberIdentifiers,
- null,
- null,
- null,
- false,
- null,
- null,
- null,
- avatar == null ? null : new File(avatar),
- null,
- null);
+ UpdateGroup.newBuilder()
+ .withName(name)
+ .withMembers(memberIdentifiers)
+ .withAvatarFile(avatar == null ? null : new File(avatar))
+ .build());
if (results != null) {
checkSendMessageResults(results.getTimestamp(), results.getResults());
}
throw new Error.Failure(message.toString());
}
+ private static List<String> getRecipientStrings(final Set<RecipientAddress> members) {
+ return members.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
+ }
+
private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
final Collection<String> recipientStrings, final String localNumber
) throws DBusExecutionException {
this.devices.clear();
}
+ private static String getGroupObjectPath(String basePath, byte[] groupId) {
+ return basePath + "/Groups/" + Base64.getEncoder()
+ .encodeToString(groupId)
+ .replace("+", "_")
+ .replace("/", "_")
+ .replace("=", "_");
+ }
+
+ private void updateGroups() {
+ List<org.asamk.signal.manager.api.Group> groups;
+ groups = m.getGroups();
+
+ unExportGroups();
+
+ groups.forEach(g -> {
+ final var object = new DbusSignalGroupImpl(g.getGroupId());
+ try {
+ connection.exportObject(object);
+ } catch (DBusException e) {
+ e.printStackTrace();
+ }
+ this.groups.add(new StructGroup(new DBusPath(object.getObjectPath()),
+ g.getGroupId().serialize(),
+ emptyIfNull(g.getTitle())));
+ });
+ }
+
+ private void unExportGroups() {
+ this.groups.stream().map(StructGroup::getObjectPath).map(DBusPath::getPath).forEach(connection::unExportObject);
+ this.groups.clear();
+ }
+
public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device {
private final org.asamk.signal.manager.api.Device device;
}
}
}
+
+ public class DbusSignalGroupImpl extends DbusProperties implements Signal.Group {
+
+ private final GroupId groupId;
+
+ public DbusSignalGroupImpl(final GroupId groupId) {
+ this.groupId = groupId;
+ super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group",
+ List.of(new DbusProperty<>("Id", groupId::serialize),
+ new DbusProperty<>("Name", () -> emptyIfNull(getGroup().getTitle()), this::setGroupName),
+ new DbusProperty<>("Description",
+ () -> emptyIfNull(getGroup().getDescription()),
+ this::setGroupDescription),
+ new DbusProperty<>("Avatar", this::setGroupAvatar),
+ new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked),
+ new DbusProperty<>("IsMember", () -> getGroup().isMember()),
+ new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()),
+ new DbusProperty<>("MessageExpirationTimer",
+ () -> getGroup().getMessageExpirationTimer(),
+ this::setMessageExpirationTime),
+ new DbusProperty<>("Members",
+ () -> new Variant<>(getRecipientStrings(getGroup().getMembers()), "as")),
+ new DbusProperty<>("PendingMembers",
+ () -> new Variant<>(getRecipientStrings(getGroup().getPendingMembers()), "as")),
+ new DbusProperty<>("RequestingMembers",
+ () -> new Variant<>(getRecipientStrings(getGroup().getRequestingMembers()), "as")),
+ new DbusProperty<>("Admins",
+ () -> new Variant<>(getRecipientStrings(getGroup().getAdminMembers()), "as")),
+ new DbusProperty<>("PermissionAddMember",
+ () -> getGroup().getPermissionAddMember().name(),
+ this::setGroupPermissionAddMember),
+ new DbusProperty<>("PermissionEditDetails",
+ () -> getGroup().getPermissionEditDetails().name(),
+ this::setGroupPermissionEditDetails),
+ new DbusProperty<>("PermissionSendMessage",
+ () -> getGroup().getPermissionSendMessage().name(),
+ this::setGroupPermissionSendMessage),
+ new DbusProperty<>("GroupInviteLink", () -> {
+ final var groupInviteLinkUrl = getGroup().getGroupInviteLinkUrl();
+ return groupInviteLinkUrl == null ? "" : groupInviteLinkUrl.getUrl();
+ }))));
+ }
+
+ @Override
+ public String getObjectPath() {
+ return getGroupObjectPath(objectPath, groupId.serialize());
+ }
+
+ @Override
+ public void quitGroup() throws Error.Failure {
+ try {
+ m.quitGroup(groupId, Set.of());
+ } catch (GroupNotFoundException | NotAGroupMemberException e) {
+ throw new Error.GroupNotFound(e.getMessage());
+ } catch (IOException e) {
+ throw new Error.Failure(e.getMessage());
+ } catch (LastGroupAdminException e) {
+ throw new Error.LastGroupAdmin(e.getMessage());
+ }
+ }
+
+ @Override
+ public void addMembers(final List<String> recipients) throws Error.Failure {
+ final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+ updateGroup(UpdateGroup.newBuilder().withMembers(memberIdentifiers).build());
+ }
+
+ @Override
+ public void removeMembers(final List<String> recipients) throws Error.Failure {
+ final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+ updateGroup(UpdateGroup.newBuilder().withRemoveMembers(memberIdentifiers).build());
+ }
+
+ @Override
+ public void addAdmins(final List<String> recipients) throws Error.Failure {
+ final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+ updateGroup(UpdateGroup.newBuilder().withAdmins(memberIdentifiers).build());
+ }
+
+ @Override
+ public void removeAdmins(final List<String> recipients) throws Error.Failure {
+ final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
+ updateGroup(UpdateGroup.newBuilder().withRemoveAdmins(memberIdentifiers).build());
+ }
+
+ @Override
+ public void resetLink() throws Error.Failure {
+ updateGroup(UpdateGroup.newBuilder().withResetGroupLink(true).build());
+ }
+
+ @Override
+ public void disableLink() throws Error.Failure {
+ updateGroup(UpdateGroup.newBuilder().withGroupLinkState(GroupLinkState.DISABLED).build());
+ }
+
+ @Override
+ public void enableLink(final boolean requiresApproval) throws Error.Failure {
+ updateGroup(UpdateGroup.newBuilder()
+ .withGroupLinkState(requiresApproval
+ ? GroupLinkState.ENABLED_WITH_APPROVAL
+ : GroupLinkState.ENABLED)
+ .build());
+ }
+
+ private org.asamk.signal.manager.api.Group getGroup() {
+ return m.getGroup(groupId);
+ }
+
+ private void setGroupName(final String name) {
+ updateGroup(UpdateGroup.newBuilder().withName(name).build());
+ }
+
+ private void setGroupDescription(final String description) {
+ updateGroup(UpdateGroup.newBuilder().withDescription(description).build());
+ }
+
+ private void setGroupAvatar(final String avatar) {
+ updateGroup(UpdateGroup.newBuilder().withAvatarFile(new File(avatar)).build());
+ }
+
+ private void setMessageExpirationTime(final int expirationTime) {
+ updateGroup(UpdateGroup.newBuilder().withExpirationTimer(expirationTime).build());
+ }
+
+ private void setGroupPermissionAddMember(final String permission) {
+ updateGroup(UpdateGroup.newBuilder().withAddMemberPermission(GroupPermission.valueOf(permission)).build());
+ }
+
+ private void setGroupPermissionEditDetails(final String permission) {
+ updateGroup(UpdateGroup.newBuilder()
+ .withEditDetailsPermission(GroupPermission.valueOf(permission))
+ .build());
+ }
+
+ private void setGroupPermissionSendMessage(final String permission) {
+ updateGroup(UpdateGroup.newBuilder()
+ .withIsAnnouncementGroup(GroupPermission.valueOf(permission) == GroupPermission.ONLY_ADMINS)
+ .build());
+ }
+
+ private void setIsBlocked(final boolean isBlocked) {
+ try {
+ m.setGroupBlocked(groupId, isBlocked);
+ } catch (NotMasterDeviceException e) {
+ throw new Error.Failure("This command doesn't work on linked devices.");
+ } catch (GroupNotFoundException e) {
+ throw new Error.GroupNotFound(e.getMessage());
+ } catch (IOException e) {
+ throw new Error.Failure(e.getMessage());
+ }
+ }
+
+ private void updateGroup(final UpdateGroup updateGroup) {
+ try {
+ m.updateGroup(groupId, updateGroup);
+ } catch (IOException e) {
+ throw new Error.Failure(e.getMessage());
+ } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
+ throw new Error.GroupNotFound(e.getMessage());
+ } catch (AttachmentInvalidException e) {
+ throw new Error.AttachmentInvalid(e.getMessage());
+ }
+ }
+ }
}