Merge branch master into dbus_mentions_attachments

This commit is contained in:
John Freed 2021-10-15 08:05:17 +02:00
commit 0583ad8f19
33 changed files with 1731 additions and 698 deletions

View file

@ -3,7 +3,7 @@ plugins {
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"

Binary file not shown.

View file

@ -1,5 +1,5 @@
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

269
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/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.
@ -17,67 +17,101 @@
#
##############################################################################
##
## 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
@ -87,9 +121,9 @@ 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
@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
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
@ -106,80 +140,95 @@ location of your Java installation."
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
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
# 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
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
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
# 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, 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.
# 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"
# For Cygwin or MSYS, switch paths to Windows format before running java
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
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
# 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
fi
# 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.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View file

@ -14,7 +14,7 @@ repositories {
}
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")

View file

@ -65,7 +65,7 @@ public class DeviceLinkInfo {
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));

View file

@ -8,13 +8,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
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;
@ -23,7 +22,6 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
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;
@ -138,20 +136,7 @@ public interface Manager extends Closeable {
) 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(
@ -198,7 +183,7 @@ public interface Manager extends Closeable {
void setGroupBlocked(
GroupId groupId, boolean blocked
) throws GroupNotFoundException, IOException;
) throws GroupNotFoundException, IOException, NotMasterDeviceException;
void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer
@ -242,8 +227,6 @@ public interface Manager extends Closeable {
boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient);
String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey);
SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address);
@Override

View file

@ -25,13 +25,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
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;
@ -39,6 +38,7 @@ import org.asamk.signal.manager.helper.AttachmentHelper;
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;
@ -60,15 +60,10 @@ import org.asamk.signal.manager.storage.stickers.Sticker;
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;
@ -99,9 +94,7 @@ import java.net.URISyntaxException;
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;
@ -113,7 +106,6 @@ import java.util.concurrent.Executors;
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;
@ -139,6 +131,7 @@ public class ManagerImpl implements Manager {
private final ContactHelper contactHelper;
private final IncomingMessageHandler incomingMessageHandler;
private final PreKeyHelper preKeyHelper;
private final IdentityHelper identityHelper;
private final Context context;
private boolean hasCaughtUpWithOldMessages = false;
@ -177,14 +170,13 @@ public class ManagerImpl implements Manager {
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,
@ -240,6 +232,11 @@ public class ManagerImpl implements Manager {
syncHelper,
this::getRecipientProfile,
jobExecutor);
this.identityHelper = new IdentityHelper(account,
dependencies,
this::resolveSignalServiceAddress,
syncHelper,
profileHelper);
}
@Override
@ -505,9 +502,12 @@ public class ManagerImpl implements Manager {
.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
@ -532,35 +532,22 @@ public class ManagerImpl implements Manager {
@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
@ -726,7 +713,10 @@ public class ManagerImpl implements Manager {
@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();
@ -793,22 +783,6 @@ public class ManagerImpl implements Manager {
}
}
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()) {
@ -1067,7 +1041,7 @@ public class ManagerImpl implements Manager {
return toGroup(groupHelper.getGroup(groupId));
}
public GroupInfo getGroupInfo(GroupId groupId) {
private GroupInfo getGroupInfo(GroupId groupId) {
return groupHelper.getGroup(groupId);
}
@ -1088,8 +1062,9 @@ public class ManagerImpl implements Manager {
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());
}
@ -1119,9 +1094,7 @@ public class ManagerImpl implements Manager {
} catch (UnregisteredUserException e) {
return false;
}
return trustIdentity(recipientId,
identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
TrustLevel.TRUSTED_VERIFIED);
return identityHelper.trustIdentityVerified(recipientId, fingerprint);
}
/**
@ -1138,10 +1111,7 @@ public class ManagerImpl implements Manager {
} 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);
}
/**
@ -1158,15 +1128,7 @@ public class ManagerImpl implements Manager {
} 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);
}
/**
@ -1182,66 +1144,13 @@ public class ManagerImpl implements Manager {
} 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
@ -1316,5 +1225,4 @@ public class ManagerImpl implements Manager {
}
account = null;
}
}

View file

@ -101,6 +101,7 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor {
if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
logger.warn("Received too many mismatch device errors, forcing new websockets.");
signalWebSocket.forceNewWebSockets();
signalWebSocket.connect();
}
}
}
@ -146,6 +147,7 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor {
+ " needed by: "
+ keepAliveRequiredSinceTime);
signalWebSocket.forceNewWebSockets();
signalWebSocket.connect();
} else {
signalWebSocket.sendKeepAlive();
}

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
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;
@ -17,9 +18,13 @@ public class Group {
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,
@ -31,9 +36,12 @@ public class Group {
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;
@ -44,9 +52,12 @@ public class Group {
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() {
@ -85,15 +96,27 @@ public class Group {
return isBlocked;
}
public int getMessageExpirationTime() {
return messageExpirationTime;
public int getMessageExpirationTimer() {
return messageExpirationTimer;
}
public boolean isAnnouncementGroup() {
return isAnnouncementGroup;
public GroupPermission getPermissionAddMember() {
return permissionAddMember;
}
public GroupPermission getPermissionEditDetails() {
return permissionEditDetails;
}
public GroupPermission getPermissionSendMessage() {
return permissionSendMessage;
}
public boolean isMember() {
return isMember;
}
public boolean isAdmin() {
return isAdmin;
}
}

View file

@ -0,0 +1,203 @@
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);
}
}
}

View file

@ -639,7 +639,7 @@ public class GroupHelper {
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
.withExpiration(g.getMessageExpirationTime());
.withExpiration(g.getMessageExpirationTimer());
}
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
@ -648,7 +648,7 @@ public class GroupHelper {
.withSignedGroupChange(signedGroupChange);
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
.withExpiration(g.getMessageExpirationTime());
.withExpiration(g.getMessageExpirationTimer());
}
private SendGroupMessageResults sendUpdateGroupV2Message(

View file

@ -0,0 +1,135 @@
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);
}
}
}

View file

@ -45,7 +45,6 @@ public final class ProfileHelper {
private final SignalAccount account;
private final SignalDependencies dependencies;
private final AvatarStore avatarStore;
private final ProfileKeyProvider profileKeyProvider;
private final UnidentifiedAccessProvider unidentifiedAccessProvider;
private final SignalServiceAddressResolver addressResolver;
@ -53,14 +52,12 @@ public final class ProfileHelper {
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;
}
@ -296,7 +293,7 @@ public final class ProfileHelper {
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);

View file

@ -1,9 +0,0 @@
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);
}

View file

@ -100,7 +100,7 @@ public class SendHelper {
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());

View file

@ -1,11 +1,16 @@
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;
@ -13,24 +18,39 @@ import static org.whispersystems.signalservice.internal.util.Util.getSecretBytes
public class UnidentifiedAccessHelper {
private final static Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
private final SignalAccount account;
private final SignalDependencies dependencies;
private final SelfProfileKeyProvider selfProfileKeyProvider;
private final ProfileKeyProvider profileKeyProvider;
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() {
@ -45,7 +65,7 @@ public class UnidentifiedAccessHelper {
switch (targetProfile.getUnidentifiedAccessMode()) {
case ENABLED:
var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
var theirProfileKey = account.getProfileStore().getProfileKey(recipient);
if (theirProfileKey == null) {
return null;
}
@ -60,7 +80,7 @@ public class UnidentifiedAccessHelper {
public Optional<UnidentifiedAccessPair> getAccessForSync() {
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
var selfUnidentifiedAccessCertificate = getSenderCertificate();
if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
return Optional.absent();
@ -82,7 +102,7 @@ public class UnidentifiedAccessHelper {
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

View file

@ -1,6 +0,0 @@
package org.asamk.signal.manager.helper;
public interface UnidentifiedAccessSenderCertificateProvider {
byte[] getSenderCertificate();
}

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups;
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;
@ -38,10 +39,16 @@ public abstract class GroupInfo {
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());
}

View file

@ -3,6 +3,7 @@ package org.asamk.signal.manager.storage.groups;
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;
@ -85,7 +86,7 @@ public class GroupInfoV1 extends GroupInfo {
}
@Override
public int getMessageExpirationTime() {
public int getMessageExpirationTimer() {
return messageExpirationTime;
}
@ -94,6 +95,21 @@ public class GroupInfoV1 extends GroupInfo {
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);
}

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups;
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;
@ -151,7 +152,7 @@ public class GroupInfoV2 extends GroupInfo {
}
@Override
public int getMessageExpirationTime() {
public int getMessageExpirationTimer() {
return this.group != null && this.group.hasDisappearingMessagesTimer()
? this.group.getDisappearingMessagesTimer().getDuration()
: 0;
@ -162,6 +163,23 @@ public class GroupInfoV2 extends GroupInfo {
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;
}
@ -169,4 +187,22 @@ public class GroupInfoV2 extends GroupInfo {
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;
}
}
}

View file

@ -29,15 +29,18 @@ method(arg1<type>, arg2<type>, ...) -> return<type>
Where <type> is according to DBus specification:
* <s> : String
* <ay> : Byte Array
* <aay> : Array of Byte Arrays
* <as> : String Array
* <ax> : Array of signed 64 bit integer
* <b> : Boolean (false|true)
* <x> : Signed 64 bit integer
* <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:)
* <o> : DBusPath object (objpath:)
* <s> : String (string:)
* <x> : Signed 64-bit (long) integer (int64:)
* <y> : Unsigned 8-bit (byte) integer (byte:)
* <> : no return value
The final parenthetical value (such as "boolean:") is the type indicator used by `dbus-send`.
Exceptions are the names of the Java Exceptions returned in the body field. They typically contain an additional message with details. All Exceptions begin with "org.asamk.Signal.Error." which is omitted here for better readability.
Phone numbers always have the format +<countrycode><regional number>
@ -45,9 +48,9 @@ Phone numbers always have the format +<countrycode><regional number>
== 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>::
@ -55,12 +58,12 @@ link(newDeviceName<s>) -> 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
@ -86,21 +89,213 @@ verify(number<s>, verificationCode<s>) -> <>::
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 (+)
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;
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
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
addMembers(recipients<as>) -> <>::
* recipients : String array of phone numbers
Add recipients to group if they are pending members; otherwise add recipients to list of requesting members.
Exceptions: Failure
disableLink() -> <>::
Disables the group's invitation link.
Exceptions: Failure
enableLink(requiresApproval<b>) -> <>::
* requiresApproval : true=add numbers using the link to the requesting members list
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, InvalidGroupId
setGroupBlocked(groupId<ay>, block<b>) -> <>::
* groupId : Byte array representing the internal group identifier
* block : false=remove block , true=blocked
Messages from blocked groups will no longer be forwarded via DBus.
Exceptions: GroupNotFound, InvalidGroupId
updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
* groupId : Byte array representing the internal group identifier
@ -110,6 +305,220 @@ updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
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: InvalidUri
sendContacts() -> <>::
Sends a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the primary device.
Exceptions: Failure
sendSyncRequest() -> <>::
Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device.
Exceptions: Failure
=== 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)
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
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`
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`
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`
removeDevice() -> <>::
Exceptions: Failure
=== Other methods
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: None
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
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>::
* 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
* 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
Depending on the type of the recipient(s) field this sends a reaction to one or multiple recipients.
Exceptions: Failure, InvalidNumber
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, 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>::
* targetSentTimestamp : Long representing timestamp of the message to delete
* 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
Depending on the type of the recipient(s) field this deletes a message with one or multiple recipients.
Exceptions: Failure, InvalidNumber
sendTyping(recipient<s>, stop<b>) -> <>::
* recipient : Phone number of a single recipient
* targetSentTimestamp : True, if typing state should be stopped
Exceptions: Failure, GroupNotFound, UntrustedIdentity
setContactBlocked(number<s>, block<b>) -> <>::
* number : Phone number affected by method
* block : false=remove block, true=blocked
Messages from blocked numbers will no longer be forwarded via DBus.
Exceptions: InvalidNumber
setContactName(number<s>,name<>) -> <>::
* number : Phone number
* name : Name to be set in contacts (in local storage with signal-cli)
Exceptions: None; for unknown numbers false is returned
isGroupBlocked(groupId<ay>) -> state<b>::
* groupId : Byte array representing the internal group identifier
* state : true=blocked, false=not blocked
Exceptions: InvalidNumber, Failure
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.
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.
Exceptions: 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.
Exception: IOErrorException
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)
@ -122,249 +531,34 @@ updateProfile(givenName<s>, familyName<s>, about<s>, aboutEmoji <s>, avatar<s>,
Exceptions: Failure
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.
Exceptions: Failure
setContactBlocked(number<s>, block<b>) -> <>::
* number : Phone number affected by method
* block : false=remove block , true=block
Messages from blocked numbers will no longer be forwarded via DBus.
Exceptions: InvalidNumber
setGroupBlocked(groupId<ay>, block<b>) -> <>::
* groupId : Byte array representing the internal group identifier
* block : false=remove block , true=blocked
Messages from blocked groups will no longer be forwarded via DBus.
Exceptions: GroupNotFound
joinGroup(inviteURI<s>) -> <>::
* inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App
Exceptions: Failure
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
isMember(groupId<ay>) -> active<b>::
* groupId : Byte array representing the internal group identifier
Note that this method does not raise an Exception for a non-existing/unknown group but will simply return false
sendEndSessionMessage(recipients<as>) -> <>::
* recipients : Array of phone numbers
Exceptions: Failure, InvalidNumber, UntrustedIdentity
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: GroupNotFound, Failure, AttachmentInvalid
sendContacts() -> <>::
Sends a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the primary device.
Exceptions: Failure
sendSyncRequest() -> <>::
Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device.
Exception: 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
Exceptions: Failure, AttachmentInvalid
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
Depending on the type of the recipient field this sends a message to one or multiple recipients.
Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity
sendTyping(recipient<s>, stop<b>) -> <>::
* recipient : Phone number of a single recipient
* targetSentTimestamp : True, if typing state should be stopped
Exceptions: Failure, GroupNotFound, UntrustedIdentity
sendReadReceipt(recipient<s>, targetSentTimestamp<ax>) -> <>::
* recipient : Phone number of a single recipient
* targetSentTimestamp : Array of Longs to identify the corresponding signal messages
Exceptions: Failure, UntrustedIdentity
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
Exceptions: Failure, InvalidNumber, GroupNotFound
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>::
* 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
* 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
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
Exceptions: Failure, GroupNotFound
sendRemoteDeleteMessage(targetSentTimestamp<x>, recipient<s>) -> timestamp<x>::
sendRemoteDeleteMessage(targetSentTimestamp<x>, recipients<as>) -> timestamp<x>::
* targetSentTimestamp : Long representing timestamp of the message to delete
* 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
Depending on the type of the recipient(s) field this deletes a message with one or multiple recipients.
Exceptions: Failure, InvalidNumber
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
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
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)
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.
isContactBlocked(number<s>) -> state<b>::
* number : Phone number
* state : true=blocked, false=not blocked
Exceptions: None; for unknown numbers false is returned
isGroupBlocked(groupId<ay>) -> state<b>::
* groupId : Byte array representing the internal group identifier
* state : true=blocked, false=not blocked
Exceptions: None; for unknown groups false is returned
removePin() -> <>::
Removes registration PIN protection.
Exception: Failure
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<as>::
* devices : String array of linked devices
Exception: Failure
removeDevice(deviceId<i>) -> <>::
* deviceId : Device ID to remove, obtained from listDevices() command
Exception: Failure
updateDeviceName(deviceName<s>) -> <>::
* deviceName : New name
Set a new name for this device (main or linked).
Exception: Failure
uploadStickerPack(stickerPackPath<s>) -> url<s>::
* stickerPackPath : Path to the manifest.json file or a zip file in the same directory
* url : URL of sticker pack after successful upload
Exception: Failure
Exceptions: Failure
version() -> version<s>::
* version : Version string of signal-cli
Exceptions: None
== Signals
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/)
SyncMessageReceived (timestamp<x>, sender<s>, destination<s>, groupId<ay>, message<s>, attachments<as>)::
* timestamp : Integer value that is used by the system to send a ReceiptReceived reply
* sender : Phone number of the sender
* destination : The DBus destination code
* 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/)
SyncMessageReceived (timestamp<x>, sender<s>, destination<s>, groupId<ay>,message<s>, attachments<as>)::
The sync message is received when the user sends a message from a linked device.
@ -373,7 +567,7 @@ MessageReceived(timestamp<x>, sender<s>, groupId<ay>, message<s>, attachments<as
* 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

View file

@ -144,7 +144,7 @@ Remove the registration lock pin.
=== 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::
@ -158,7 +158,8 @@ Only works, if this is the master device.
*--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

View file

@ -1,9 +1,13 @@
package org.asamk;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.dbus.DbusAttachment;
import org.asamk.signal.dbus.DbusMention;
import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.Struct;
import org.freedesktop.dbus.annotations.DBusProperty;
import org.freedesktop.dbus.annotations.Position;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.freedesktop.dbus.interfaces.DBusInterface;
@ -84,14 +88,27 @@ public interface Signal extends DBusInterface {
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;
@ -106,7 +123,7 @@ public interface Signal extends DBusInterface {
DBusPath getDevice(long deviceId);
List<DBusPath> listDevices() throws Error.Failure;
List<StructDevice> listDevices() throws Error.Failure;
DBusPath getThisDevice();
@ -133,18 +150,23 @@ public interface Signal extends DBusInterface {
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;
String uploadStickerPack(String stickerPackPath) throws Error.Failure;
void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException;
class MessageReceived extends DBusSignal {
private final long timestamp;
@ -417,7 +439,37 @@ public interface Signal extends DBusInterface {
}
}
@DBusProperty(name = "Id", type = Integer.class, access = DBusProperty.Access.READ)
class StructDevice extends Struct {
@Position(0)
DBusPath objectPath;
@Position(1)
Long id;
@Position(2)
String name;
public StructDevice(final DBusPath objectPath, final Long id, final String name) {
this.objectPath = objectPath;
this.id = id;
this.name = name;
}
public DBusPath getObjectPath() {
return objectPath;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@DBusProperty(name = "Id", type = Long.class, access = DBusProperty.Access.READ)
@DBusProperty(name = "Name", type = String.class)
@DBusProperty(name = "Created", type = String.class, access = DBusProperty.Access.READ)
@DBusProperty(name = "LastSeen", type = String.class, access = DBusProperty.Access.READ)
@ -426,6 +478,71 @@ public interface Signal extends DBusInterface {
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 {
@ -449,6 +566,13 @@ public interface Signal extends DBusInterface {
}
}
class DeviceNotFound extends DBusExecutionException {
public DeviceNotFound(final String message) {
super(message);
}
}
class GroupNotFound extends DBusExecutionException {
public GroupNotFound(final String message) {
@ -463,6 +587,13 @@ public interface Signal extends DBusInterface {
}
}
class LastGroupAdmin extends DBusExecutionException {
public LastGroupAdmin(final String message) {
super(message);
}
}
class InvalidNumber extends DBusExecutionException {
public InvalidNumber(final String message) {

View file

@ -6,7 +6,6 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
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;
@ -377,9 +376,6 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
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:");

View file

@ -52,6 +52,8 @@ public class BlockCommand implements JsonRpcLocalCommand {
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) {

View file

@ -63,7 +63,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
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: {}",
@ -91,11 +91,14 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
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());
@ -122,6 +125,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
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(
@ -135,6 +141,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
Set<JsonGroupMember> pendingMembers,
Set<JsonGroupMember> requestingMembers,
Set<JsonGroupMember> admins,
final String permissionAddMember,
final String permissionEditDetails,
final String permissionSendMessage,
String groupInviteLink
) {
this.id = id;
@ -148,6 +157,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
this.pendingMembers = pendingMembers;
this.requestingMembers = requestingMembers;
this.admins = admins;
this.permissionAddMember = permissionAddMember;
this.permissionEditDetails = permissionEditDetails;
this.permissionSendMessage = permissionSendMessage;
this.groupInviteLink = groupInviteLink;
}
}

View file

@ -51,6 +51,8 @@ public class UnblockCommand implements JsonRpcLocalCommand {
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) {

View file

@ -12,6 +12,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
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;
@ -145,21 +146,23 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand {
}
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());

View file

@ -15,9 +15,9 @@ import org.asamk.signal.manager.api.RecipientIdentifier;
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;
@ -30,7 +30,6 @@ import org.freedesktop.dbus.DBusPath;
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;
@ -145,13 +144,14 @@ public class DbusManagerImpl implements Manager {
@Override
public List<Device> getLinkedDevices() throws IOException {
final var thisDevice = signal.getThisDevice();
return signal.listDevices().stream().map(devicePath -> {
final var device = getRemoteObject(devicePath, Signal.Device.class).GetAll("org.asamk.Signal.Device");
return signal.listDevices().stream().map(d -> {
final var device = getRemoteObject(d.getObjectPath(),
Signal.Device.class).GetAll("org.asamk.Signal.Device");
return new Device((long) device.get("Id").getValue(),
(String) device.get("Name").getValue(),
(long) device.get("Created").getValue(),
(long) device.get("LastSeen").getValue(),
thisDevice.equals(devicePath));
thisDevice.equals(d.getObjectPath()));
}).collect(Collectors.toList());
}
@ -182,8 +182,8 @@ public class DbusManagerImpl implements Manager {
@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
@ -193,7 +193,8 @@ public class DbusManagerImpl implements Manager {
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());
}
@ -206,8 +207,7 @@ public class DbusManagerImpl implements Manager {
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()));
@ -215,25 +215,76 @@ public class DbusManagerImpl implements Manager {
@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());
}
@ -343,7 +394,12 @@ public class DbusManagerImpl implements Manager {
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
@ -410,19 +466,41 @@ public class DbusManagerImpl implements Manager {
@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
@ -459,13 +537,6 @@ public class DbusManagerImpl implements Manager {
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;

View file

@ -51,6 +51,7 @@ public abstract class DbusProperties implements Properties {
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Variant<?>> GetAll(final String interface_name) {
final var handler = getHandlerOptional(interface_name);
if (handler.isEmpty()) {
@ -61,6 +62,9 @@ public abstract class DbusProperties implements Properties {
.getProperties()
.stream()
.filter(p -> p.getGetter() != null)
.collect(Collectors.toMap(DbusProperty::getName, p -> new Variant<>(p.getGetter().get())));
.collect(Collectors.toMap(DbusProperty::getName, p -> {
final Object o = p.getGetter().get();
return o instanceof Variant ? (Variant<Object>) o : new Variant<>(o);
}));
}
}

View file

@ -21,6 +21,12 @@ public class DbusProperty<T> {
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;
}

View file

@ -2,6 +2,7 @@ package org.asamk.signal.dbus;
import org.asamk.Signal;
import org.asamk.signal.BaseConfig;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotMasterDeviceException;
@ -11,9 +12,12 @@ import org.asamk.signal.manager.api.Identity;
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;
@ -24,6 +28,7 @@ import org.freedesktop.dbus.DBusPath;
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;
@ -38,6 +43,8 @@ import java.io.IOException;
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;
@ -55,7 +62,8 @@ public class DbusSignalImpl implements Signal {
private final String objectPath;
private DBusPath thisDevice;
private final List<DBusPath> devices = new ArrayList<>();
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;
@ -65,6 +73,7 @@ public class DbusSignalImpl implements Signal {
public void initObjects() {
updateDevices();
updateGroups();
}
public void close() {
@ -81,6 +90,18 @@ public class DbusSignalImpl implements Signal {
return m.getSelfNumber();
}
@Override
public void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException {
final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", "");
try {
m.submitRateLimitRecaptchaChallenge(challenge, captcha);
} catch (IOException e) {
throw new IOErrorException("Submit challenge error: " + e.getMessage(), e);
}
}
@Override
public void addDevice(String uri) {
try {
@ -97,45 +118,19 @@ public class DbusSignalImpl implements Signal {
@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 List<DBusPath> listDevices() {
public List<StructDevice> listDevices() {
updateDevices();
return this.devices;
}
private void updateDevices() {
List<org.asamk.signal.manager.api.Device> linkedDevices;
try {
linkedDevices = m.getLinkedDevices();
} catch (IOException | Error.Failure e) {
throw new Error.Failure("Failed to get linked devices: " + e.getMessage());
}
unExportDevices();
linkedDevices.forEach(d -> {
final var object = new DbusSignalDeviceImpl(d);
final var deviceObjectPath = object.getObjectPath();
try {
connection.exportObject(object);
} catch (DBusException e) {
e.printStackTrace();
}
if (d.isThisDevice()) {
thisDevice = new DBusPath(deviceObjectPath);
}
this.devices.add(new DBusPath(deviceObjectPath));
});
}
private void unExportDevices() {
this.devices.stream().map(DBusPath::getPath).forEach(connection::unExportObject);
this.devices.clear();
}
@Override
public DBusPath getThisDevice() {
updateDevices();
@ -410,6 +405,8 @@ public class DbusSignalImpl implements Signal {
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) {
@ -427,6 +424,22 @@ public class DbusSignalImpl implements Signal {
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));
@ -443,10 +456,18 @@ public class DbusSignalImpl implements Signal {
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 {
@ -460,19 +481,11 @@ public class DbusSignalImpl implements Signal {
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());
}
@ -752,6 +765,10 @@ public class DbusSignalImpl implements Signal {
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 {
@ -788,21 +805,87 @@ public class DbusSignalImpl implements Signal {
return name.isEmpty() ? null : name;
}
private String emptyIfNull(final String string) {
return string == null ? "" : string;
}
private static String getDeviceObjectPath(String basePath, long deviceId) {
return basePath + "/Devices/" + deviceId;
}
private void updateDevices() {
List<org.asamk.signal.manager.api.Device> linkedDevices;
try {
linkedDevices = m.getLinkedDevices();
} catch (IOException e) {
throw new Error.Failure("Failed to get linked devices: " + e.getMessage());
}
unExportDevices();
linkedDevices.forEach(d -> {
final var object = new DbusSignalDeviceImpl(d);
final var deviceObjectPath = object.getObjectPath();
try {
connection.exportObject(object);
} catch (DBusException e) {
e.printStackTrace();
}
if (d.isThisDevice()) {
thisDevice = new DBusPath(deviceObjectPath);
}
this.devices.add(new StructDevice(new DBusPath(deviceObjectPath), d.getId(), emptyIfNull(d.getName())));
});
}
private void unExportDevices() {
this.devices.stream()
.map(StructDevice::getObjectPath)
.map(DBusPath::getPath)
.forEach(connection::unExportObject);
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 DbusSignalDeviceImpl(final org.asamk.signal.manager.api.Device device) {
super();
super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device",
List.of(new DbusProperty<>("Id", device::getId),
new DbusProperty<>("Name",
() -> device.getName() == null ? "" : device.getName(),
this::setDeviceName),
new DbusProperty<>("Name", () -> emptyIfNull(device.getName()), this::setDeviceName),
new DbusProperty<>("Created", device::getCreated),
new DbusProperty<>("LastSeen", device::getLastSeen))));
this.device = device;
@ -836,4 +919,168 @@ public class DbusSignalImpl implements Signal {
}
}
}
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());
}
}
}
}