diff --git a/.gitignore b/.gitignore
index 18b11c03..1749973c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,4 @@ local.properties
.settings/
out/
.DS_Store
-.asciidoctorconfig.adoc
-patches/
-signal-cli
/bin/
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index dc6c910e..6e528805 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -14,7 +14,7 @@ repositories {
}
dependencies {
- api("com.github.turasa:signal-service-java:2.15.3_unofficial_27")
+ api("com.github.turasa:signal-service-java:2.15.3_unofficial_28")
implementation("com.google.protobuf:protobuf-javalite:3.10.0")
implementation("org.bouncycastle:bcprov-jdk15on:1.69")
implementation("org.slf4j:slf4j-api:1.7.30")
diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java
index 05700379..7a421966 100644
--- a/lib/src/main/java/org/asamk/signal/manager/Manager.java
+++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java
@@ -1,23 +1,8 @@
-/*
- Copyright (C) 2015-2021 AsamK and contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
- */
package org.asamk.signal.manager;
-import org.asamk.signal.manager.actions.HandleAction;
import org.asamk.signal.manager.api.Device;
+import org.asamk.signal.manager.api.Group;
+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.SendGroupMessageResults;
@@ -25,7 +10,6 @@ import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment;
-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;
@@ -34,241 +18,52 @@ import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
-import org.asamk.signal.manager.helper.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.IncomingMessageHandler;
-import org.asamk.signal.manager.helper.PinHelper;
-import org.asamk.signal.manager.helper.PreKeyHelper;
-import org.asamk.signal.manager.helper.ProfileHelper;
-import org.asamk.signal.manager.helper.SendHelper;
-import org.asamk.signal.manager.helper.StorageHelper;
-import org.asamk.signal.manager.helper.SyncHelper;
-import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
-import org.asamk.signal.manager.jobs.Context;
import org.asamk.signal.manager.storage.SignalAccount;
-import org.asamk.signal.manager.storage.groups.GroupInfo;
-import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
-import org.asamk.signal.manager.storage.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile;
-import org.asamk.signal.manager.storage.recipients.RecipientId;
-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.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
-import org.whispersystems.libsignal.ecc.ECPublicKey;
-import org.whispersystems.libsignal.fingerprint.Fingerprint;
-import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
-import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
-import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
-import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
-import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
-import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
-import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
-import org.whispersystems.signalservice.api.util.DeviceNameUtil;
-import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
-import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
-import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
-import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
-import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
-import org.whispersystems.signalservice.internal.util.Hex;
-import org.whispersystems.signalservice.internal.util.Util;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.URI;
-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;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.ExecutorService;
-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;
+public interface Manager extends Closeable {
-public class Manager implements Closeable {
-
- private final static Logger logger = LoggerFactory.getLogger(Manager.class);
-
- private final ServiceEnvironmentConfig serviceEnvironmentConfig;
- private final SignalDependencies dependencies;
-
- private SignalAccount account;
-
- private final ExecutorService executor = Executors.newCachedThreadPool();
-
- private final ProfileHelper profileHelper;
- private final PinHelper pinHelper;
- private final StorageHelper storageHelper;
- private final SendHelper sendHelper;
- private final SyncHelper syncHelper;
- private final AttachmentHelper attachmentHelper;
- private final GroupHelper groupHelper;
- private final ContactHelper contactHelper;
- private final IncomingMessageHandler incomingMessageHandler;
- private final PreKeyHelper preKeyHelper;
-
- private final Context context;
- private boolean hasCaughtUpWithOldMessages = false;
-
- Manager(
- SignalAccount account,
- PathConfig pathConfig,
- ServiceEnvironmentConfig serviceEnvironmentConfig,
- String userAgent
- ) {
- this.account = account;
- this.serviceEnvironmentConfig = serviceEnvironmentConfig;
-
- final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(),
- account.getUsername(),
- account.getPassword(),
- account.getDeviceId());
- final var sessionLock = new SignalSessionLock() {
- private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
-
- @Override
- public Lock acquire() {
- LEGACY_LOCK.lock();
- return LEGACY_LOCK::unlock;
- }
- };
- this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
- userAgent,
- credentialsProvider,
- account.getSignalProtocolStore(),
- executor,
- sessionLock);
- final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
- final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
- final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
-
- 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);
- this.profileHelper = new ProfileHelper(account,
- dependencies,
- avatarStore,
- account.getProfileStore()::getProfileKey,
- unidentifiedAccessHelper::getAccessFor,
- this::resolveSignalServiceAddress);
- final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
- this::getRecipientProfile,
- account::getSelfRecipientId,
- dependencies.getGroupsV2Operations(),
- dependencies.getGroupsV2Api(),
- this::resolveSignalServiceAddress);
- this.sendHelper = new SendHelper(account,
- dependencies,
- unidentifiedAccessHelper,
- this::resolveSignalServiceAddress,
- account.getRecipientStore(),
- this::handleIdentityFailure,
- this::getGroup,
- this::refreshRegisteredUser);
- this.groupHelper = new GroupHelper(account,
- dependencies,
- attachmentHelper,
- sendHelper,
- groupV2Helper,
- avatarStore,
- this::resolveSignalServiceAddress,
- account.getRecipientStore());
- this.storageHelper = new StorageHelper(account, dependencies, groupHelper);
- this.contactHelper = new ContactHelper(account);
- this.syncHelper = new SyncHelper(account,
- attachmentHelper,
- sendHelper,
- groupHelper,
- avatarStore,
- this::resolveSignalServiceAddress);
- preKeyHelper = new PreKeyHelper(account, dependencies);
-
- this.context = new Context(account,
- dependencies,
- stickerPackStore,
- sendHelper,
- groupHelper,
- syncHelper,
- profileHelper,
- storageHelper,
- preKeyHelper);
- var jobExecutor = new JobExecutor(context);
-
- this.incomingMessageHandler = new IncomingMessageHandler(account,
- dependencies,
- account.getRecipientStore(),
- this::resolveSignalServiceAddress,
- groupHelper,
- contactHelper,
- attachmentHelper,
- syncHelper,
- this::getRecipientProfile,
- jobExecutor);
- }
-
- public String getUsername() {
- return account.getUsername();
- }
-
- public RecipientId getSelfRecipientId() {
- return account.getSelfRecipientId();
- }
-
- public int getDeviceId() {
- return account.getDeviceId();
- }
-
- public static Manager init(
- String username,
+ static Manager init(
+ String number,
File settingsPath,
ServiceEnvironment serviceEnvironment,
String userAgent,
- final TrustNewIdentity trustNewIdentity
+ TrustNewIdentity trustNewIdentity
) throws IOException, NotRegisteredException {
var pathConfig = PathConfig.createDefault(settingsPath);
- if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
+ if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) {
throw new NotRegisteredException();
}
- var account = SignalAccount.load(pathConfig.getDataPath(), username, true, trustNewIdentity);
+ var account = SignalAccount.load(pathConfig.getDataPath(), number, true, trustNewIdentity);
if (!account.isRegistered()) {
throw new NotRegisteredException();
@@ -276,10 +71,10 @@ public class Manager implements Closeable {
final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
- return new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent);
+ return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
}
- public static List getAllLocalUsernames(File settingsPath) {
+ static List getAllLocalNumbers(File settingsPath) {
var pathConfig = PathConfig.createDefault(settingsPath);
final var dataPath = pathConfig.getDataPath();
final var files = dataPath.listFiles();
@@ -295,208 +90,54 @@ public class Manager implements Closeable {
.collect(Collectors.toList());
}
- public void checkAccountState() throws IOException {
- if (account.getLastReceiveTimestamp() == 0) {
- logger.info("The Signal protocol expects that incoming messages are regularly received.");
- } else {
- var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
- long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
- if (days > 7) {
- logger.warn(
- "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
- days);
- }
- }
- preKeyHelper.refreshPreKeysIfNecessary();
- if (account.getUuid() == null) {
- account.setUuid(dependencies.getAccountManager().getOwnUuid());
- }
- updateAccountAttributes(null);
- }
+ String getSelfNumber();
- /**
- * This is used for checking a set of phone numbers for registration on Signal
- *
- * @param numbers The set of phone number in question
- * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
- * @throws IOException if its unable to get the contacts to check if they're registered
- */
- public Map> areUsersRegistered(Set numbers) throws IOException {
- Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
- try {
- return PhoneNumberFormatter.formatNumber(n, account.getUsername());
- } catch (InvalidNumberException e) {
- return "";
- }
- }));
+ void checkAccountState() throws IOException;
- // Note "registeredUsers" has no optionals. It only gives us info on users who are registered
- var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values()
- .stream()
- .filter(s -> !s.isEmpty())
- .collect(Collectors.toSet()));
+ Map> areUsersRegistered(Set numbers) throws IOException;
- return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
- final var number = canonicalizedNumbers.get(n);
- final var uuid = registeredUsers.get(number);
- return new Pair<>(number.isEmpty() ? null : number, uuid);
- }));
- }
+ void updateAccountAttributes(String deviceName) throws IOException;
- public void updateAccountAttributes(String deviceName) throws IOException {
- final String encryptedDeviceName;
- if (deviceName == null) {
- encryptedDeviceName = account.getEncryptedDeviceName();
- } else {
- final var privateKey = account.getIdentityKeyPair().getPrivateKey();
- encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
- account.setEncryptedDeviceName(encryptedDeviceName);
- }
- dependencies.getAccountManager()
- .setAccountAttributes(encryptedDeviceName,
- null,
- account.getLocalRegistrationId(),
- true,
- null,
- account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
- account.getSelfUnidentifiedAccessKey(),
- account.isUnrestrictedUnidentifiedAccess(),
- capabilities,
- account.isDiscoverableByPhoneNumber());
- }
+ void updateConfiguration(
+ final Boolean readReceipts,
+ final Boolean unidentifiedDeliveryIndicators,
+ final Boolean typingIndicators,
+ final Boolean linkPreviews
+ ) throws IOException, NotMasterDeviceException;
- /**
- * @param givenName if null, the previous givenName will be kept
- * @param familyName if null, the previous familyName will be kept
- * @param about if null, the previous about text will be kept
- * @param aboutEmoji if null, the previous about emoji will be kept
- * @param avatar if avatar is null the image from the local avatar store is used (if present),
- */
- public void setProfile(
- String givenName, final String familyName, String about, String aboutEmoji, Optional avatar
- ) throws IOException {
- profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar);
- syncHelper.sendSyncFetchProfileMessage();
- }
+ void setProfile(
+ String givenName, String familyName, String about, String aboutEmoji, Optional avatar
+ ) throws IOException;
- public void unregister() throws IOException {
- // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
- // If this is the master device, other users can't send messages to this number anymore.
- // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
- dependencies.getAccountManager().setGcmId(Optional.absent());
+ void unregister() throws IOException;
- account.setRegistered(false);
- }
+ void deleteAccount() throws IOException;
- public void deleteAccount() throws IOException {
- try {
- pinHelper.removeRegistrationLockPin();
- } catch (UnauthenticatedResponseException e) {
- logger.warn("Failed to remove registration lock pin");
- }
- account.setRegistrationLockPin(null, null);
+ void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException;
- dependencies.getAccountManager().deleteAccount();
+ List getLinkedDevices() throws IOException;
- account.setRegistered(false);
- }
+ void removeLinkedDevices(long deviceId) throws IOException;
- public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
- dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
- }
+ void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException;
- public List getLinkedDevices() throws IOException {
- var devices = dependencies.getAccountManager().getDevices();
- account.setMultiDevice(devices.size() > 1);
- var identityKey = account.getIdentityKeyPair().getPrivateKey();
- return devices.stream().map(d -> {
- String deviceName = d.getName();
- if (deviceName != null) {
- try {
- deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
- } catch (IOException e) {
- logger.debug("Failed to decrypt device name, maybe plain text?", e);
- }
- }
- return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen());
- }).collect(Collectors.toList());
- }
+ void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException;
- public void removeLinkedDevices(int deviceId) throws IOException {
- dependencies.getAccountManager().removeDevice(deviceId);
- var devices = dependencies.getAccountManager().getDevices();
- account.setMultiDevice(devices.size() > 1);
- }
+ Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException;
- public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
- var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
+ List getGroups();
- addDevice(info.deviceIdentifier, info.deviceKey);
- }
-
- private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
- var identityKeyPair = account.getIdentityKeyPair();
- var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
-
- dependencies.getAccountManager()
- .addDevice(deviceIdentifier,
- deviceKey,
- identityKeyPair,
- Optional.of(account.getProfileKey().serialize()),
- verificationCode);
- account.setMultiDevice(true);
- }
-
- public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException {
- if (!account.isMasterDevice()) {
- throw new RuntimeException("Only master device can set a PIN");
- }
- if (pin.isPresent()) {
- final var masterKey = account.getPinMasterKey() != null
- ? account.getPinMasterKey()
- : KeyUtils.createMasterKey();
-
- pinHelper.setRegistrationLockPin(pin.get(), masterKey);
-
- account.setRegistrationLockPin(pin.get(), masterKey);
- } else {
- // Remove KBS Pin
- pinHelper.removeRegistrationLockPin();
-
- account.setRegistrationLockPin(null, null);
- }
- }
-
- void refreshPreKeys() throws IOException {
- preKeyHelper.refreshPreKeys();
- }
-
- public Profile getRecipientProfile(RecipientId recipientId) {
- return profileHelper.getRecipientProfile(recipientId);
- }
-
- public List getGroups() {
- return account.getGroupStore().getGroups();
- }
-
- public SendGroupMessageResults quitGroup(
+ SendGroupMessageResults quitGroup(
GroupId groupId, Set groupAdmins
- ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
- final var newAdmins = resolveRecipients(groupAdmins);
- return groupHelper.quitGroup(groupId, newAdmins);
- }
+ ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException;
- public void deleteGroup(GroupId groupId) throws IOException {
- groupHelper.deleteGroup(groupId);
- }
+ void deleteGroup(GroupId groupId) throws IOException;
- public Pair createGroup(
+ Pair createGroup(
String name, Set members, File avatarFile
- ) throws IOException, AttachmentInvalidException {
- return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile);
- }
+ ) throws IOException, AttachmentInvalidException;
- public SendGroupMessageResults updateGroup(
+ SendGroupMessageResults updateGroup(
GroupId groupId,
String name,
String description,
@@ -511,724 +152,104 @@ public class Manager implements Closeable {
File avatarFile,
Integer expirationTimer,
Boolean isAnnouncementGroup
- ) 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);
- }
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException;
- public Pair joinGroup(
+ Pair joinGroup(
GroupInviteLinkUrl inviteLinkUrl
- ) throws IOException, GroupLinkNotActiveException {
- return groupHelper.joinGroup(inviteLinkUrl);
- }
+ ) throws IOException, GroupLinkNotActiveException;
- public SendMessageResults sendMessage(
- SignalServiceDataMessage.Builder messageBuilder, Set recipients
- ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
- var results = new HashMap>();
- long timestamp = System.currentTimeMillis();
- messageBuilder.withTimestamp(timestamp);
- for (final var recipient : recipients) {
- if (recipient instanceof RecipientIdentifier.Single) {
- final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient);
- final var result = sendHelper.sendMessage(messageBuilder, recipientId);
- results.put(recipient, List.of(result));
- } else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
- final var result = sendHelper.sendSelfMessage(messageBuilder);
- results.put(recipient, List.of(result));
- } else if (recipient instanceof RecipientIdentifier.Group) {
- final var groupId = ((RecipientIdentifier.Group) recipient).groupId;
- final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId);
- results.put(recipient, result);
- }
- }
- return new SendMessageResults(timestamp, results);
- }
+ void sendTypingMessage(
+ TypingAction action, Set recipients
+ ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
- public void sendTypingMessage(
- SignalServiceTypingMessage.Action action, Set recipients
- ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
- final var timestamp = System.currentTimeMillis();
- for (var recipient : recipients) {
- if (recipient instanceof RecipientIdentifier.Single) {
- final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent());
- final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient);
- sendHelper.sendTypingMessage(message, recipientId);
- } else if (recipient instanceof RecipientIdentifier.Group) {
- final var groupId = ((RecipientIdentifier.Group) recipient).groupId;
- final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize()));
- sendHelper.sendGroupTypingMessage(message, groupId);
- }
- }
- }
-
- public void sendReadReceipt(
+ void sendReadReceipt(
RecipientIdentifier.Single sender, List messageIds
- ) throws IOException, UntrustedIdentityException {
- var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
- messageIds,
- System.currentTimeMillis());
+ ) throws IOException, UntrustedIdentityException;
- sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
- }
-
- public void sendViewedReceipt(
+ void sendViewedReceipt(
RecipientIdentifier.Single sender, List messageIds
- ) throws IOException, UntrustedIdentityException {
- var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
- messageIds,
- System.currentTimeMillis());
+ ) throws IOException, UntrustedIdentityException;
- sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
- }
-
- public SendMessageResults sendMessage(
+ SendMessageResults sendMessage(
Message message, Set recipients
- ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
- final var messageBuilder = SignalServiceDataMessage.newBuilder();
- applyMessage(messageBuilder, message);
- return sendMessage(messageBuilder, recipients);
- }
+ ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
- private void applyMessage(
- final SignalServiceDataMessage.Builder messageBuilder, final Message message
- ) throws AttachmentInvalidException, IOException {
- messageBuilder.withBody(message.getMessageText());
- final var attachments = message.getAttachments();
- if (attachments != null) {
- messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments));
- }
- }
-
- public SendMessageResults sendRemoteDeleteMessage(
+ SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set recipients
- ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
- var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
- final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
- return sendMessage(messageBuilder, recipients);
- }
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
- public SendMessageResults sendMessageReaction(
+ SendMessageResults sendMessageReaction(
String emoji,
boolean remove,
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set recipients
- ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
- var targetAuthorRecipientId = resolveRecipient(targetAuthor);
- var reaction = new SignalServiceDataMessage.Reaction(emoji,
- remove,
- resolveSignalServiceAddress(targetAuthorRecipientId),
- targetSentTimestamp);
- final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction);
- return sendMessage(messageBuilder, recipients);
- }
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
- public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException {
- var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
+ SendMessageResults sendEndSessionMessage(Set recipients) throws IOException;
- try {
- return sendMessage(messageBuilder,
- recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()));
- } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
- throw new AssertionError(e);
- } finally {
- for (var recipient : recipients) {
- final var recipientId = resolveRecipient(recipient);
- account.getSessionStore().deleteAllSessions(recipientId);
- }
- }
- }
-
- public void setContactName(
+ void setContactName(
RecipientIdentifier.Single recipient, String name
- ) throws NotMasterDeviceException, UnregisteredUserException {
- if (!account.isMasterDevice()) {
- throw new NotMasterDeviceException();
- }
- contactHelper.setContactName(resolveRecipient(recipient), name);
- }
+ ) throws NotMasterDeviceException, UnregisteredUserException;
- public void setContactBlocked(
+ void setContactBlocked(
RecipientIdentifier.Single recipient, boolean blocked
- ) throws NotMasterDeviceException, IOException {
- if (!account.isMasterDevice()) {
- throw new NotMasterDeviceException();
- }
- contactHelper.setContactBlocked(resolveRecipient(recipient), blocked);
- // TODO cycle our profile key
- syncHelper.sendBlockedList();
- }
+ ) throws NotMasterDeviceException, IOException;
- public void setGroupBlocked(
- final GroupId groupId, final boolean blocked
- ) throws GroupNotFoundException, IOException {
- groupHelper.setGroupBlocked(groupId, blocked);
- // TODO cycle our profile key
- syncHelper.sendBlockedList();
- }
+ void setGroupBlocked(
+ GroupId groupId, boolean blocked
+ ) throws GroupNotFoundException, IOException;
- /**
- * Change the expiration timer for a contact
- */
- public void setExpirationTimer(
+ void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer
- ) throws IOException {
- var recipientId = resolveRecipient(recipient);
- contactHelper.setExpirationTimer(recipientId, messageExpirationTimer);
- final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
- try {
- sendMessage(messageBuilder, Set.of(recipient));
- } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) {
- throw new AssertionError(e);
- }
- }
+ ) throws IOException;
- /**
- * Upload the sticker pack from path.
- *
- * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
- * @return if successful, returns the URL to install the sticker pack in the signal app
- */
- public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
- var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
+ URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException;
- var messageSender = dependencies.getMessageSender();
+ void requestAllSyncData() throws IOException;
- var packKey = KeyUtils.createStickerUploadKey();
- var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
- var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
-
- var sticker = new Sticker(packId, packKey);
- account.getStickerStore().updateSticker(sticker);
-
- try {
- return new URI("https",
- "signal.art",
- "/addstickers/",
- "pack_id="
- + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8)
- + "&pack_key="
- + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8));
- } catch (URISyntaxException e) {
- throw new AssertionError(e);
- }
- }
-
- public void requestAllSyncData() throws IOException {
- syncHelper.requestAllSyncData();
- retrieveRemoteStorage();
- }
-
- void retrieveRemoteStorage() throws IOException {
- if (account.getStorageKey() != null) {
- storageHelper.readDataFromStorage();
- }
- }
-
- private byte[] getSenderCertificate() {
- byte[] certificate;
- try {
- if (account.isPhoneNumberShared()) {
- certificate = dependencies.getAccountManager().getSenderCertificate();
- } else {
- certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
- }
- } catch (IOException e) {
- logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
- return null;
- }
- // TODO cache for a day
- return certificate;
- }
-
- private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException {
- final var address = resolveSignalServiceAddress(recipientId);
- if (!address.getNumber().isPresent()) {
- return recipientId;
- }
- final var number = address.getNumber().get();
- final var uuid = getRegisteredUser(number);
- return resolveRecipientTrusted(new SignalServiceAddress(uuid, number));
- }
-
- private UUID getRegisteredUser(final String number) throws IOException {
- final Map uuidMap;
- try {
- uuidMap = getRegisteredUsers(Set.of(number));
- } catch (NumberFormatException e) {
- throw new UnregisteredUserException(number, e);
- }
- final var uuid = uuidMap.get(number);
- if (uuid == null) {
- throw new UnregisteredUserException(number, null);
- }
- return uuid;
- }
-
- private Map getRegisteredUsers(final Set numbers) throws IOException {
- final Map registeredUsers;
- try {
- registeredUsers = dependencies.getAccountManager()
- .getRegisteredUsers(ServiceConfig.getIasKeyStore(),
- numbers,
- serviceEnvironmentConfig.getCdsMrenclave());
- } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) {
- throw new IOException(e);
- }
-
- // Store numbers as recipients so we have the number/uuid association
- registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number)));
-
- return registeredUsers;
- }
-
- public void sendTypingMessage(
- TypingAction action, Set recipients
- ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
- sendTypingMessage(action.toSignalService(), recipients);
- }
-
- private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
- Set queuedActions = new HashSet<>();
- for (var cachedMessage : account.getMessageCache().getCachedMessages()) {
- var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage);
- if (actions != null) {
- queuedActions.addAll(actions);
- }
- }
- handleQueuedActions(queuedActions);
- }
-
- private List retryFailedReceivedMessage(
- final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage
- ) {
- var envelope = cachedMessage.loadEnvelope();
- if (envelope == null) {
- cachedMessage.delete();
- return null;
- }
-
- final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler);
- final var actions = result.first();
- final var exception = result.second();
-
- if (exception instanceof UntrustedIdentityException) {
- if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) {
- // Envelope is more than a month old, cleaning up.
- cachedMessage.delete();
- return null;
- }
- if (!envelope.hasSourceUuid()) {
- final var identifier = ((UntrustedIdentityException) exception).getSender();
- final var recipientId = account.getRecipientStore().resolveRecipient(identifier);
- try {
- account.getMessageCache().replaceSender(cachedMessage, recipientId);
- } catch (IOException ioException) {
- logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage());
- }
- }
- return null;
- }
-
- // If successful and for all other errors that are not recoverable, delete the cached message
- cachedMessage.delete();
- return actions;
- }
-
- public void receiveMessages(
+ void receiveMessages(
long timeout,
TimeUnit unit,
boolean returnOnTimeout,
boolean ignoreAttachments,
ReceiveMessageHandler handler
- ) throws IOException {
- retryFailedReceivedMessages(handler, ignoreAttachments);
+ ) throws IOException;
- Set queuedActions = new HashSet<>();
+ boolean hasCaughtUpWithOldMessages();
- final var signalWebSocket = dependencies.getSignalWebSocket();
- signalWebSocket.connect();
+ boolean isContactBlocked(RecipientIdentifier.Single recipient);
- hasCaughtUpWithOldMessages = false;
+ File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId);
- while (!Thread.interrupted()) {
- SignalServiceEnvelope envelope;
- final CachedMessage[] cachedMessage = {null};
- account.setLastReceiveTimestamp(System.currentTimeMillis());
- logger.debug("Checking for new message from server");
- try {
- var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> {
- final var recipientId = envelope1.hasSourceUuid()
- ? resolveRecipient(envelope1.getSourceAddress())
- : null;
- // store message on disk, before acknowledging receipt to the server
- cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
- });
- if (result.isPresent()) {
- envelope = result.get();
- logger.debug("New message received from server");
- } else {
- logger.debug("Received indicator that server queue is empty");
- handleQueuedActions(queuedActions);
- queuedActions.clear();
+ void sendContacts() throws IOException;
- hasCaughtUpWithOldMessages = true;
- synchronized (this) {
- this.notifyAll();
- }
+ List> getContacts();
- // Continue to wait another timeout for new messages
- continue;
- }
- } catch (AssertionError e) {
- if (e.getCause() instanceof InterruptedException) {
- Thread.currentThread().interrupt();
- break;
- } else {
- throw e;
- }
- } catch (WebSocketUnavailableException e) {
- logger.debug("Pipe unexpectedly unavailable, connecting");
- signalWebSocket.connect();
- continue;
- } catch (TimeoutException e) {
- if (returnOnTimeout) return;
- continue;
- }
+ String getContactOrProfileName(RecipientIdentifier.Single recipient);
- final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler);
- queuedActions.addAll(result.first());
- final var exception = result.second();
+ Group getGroup(GroupId groupId);
- if (hasCaughtUpWithOldMessages) {
- handleQueuedActions(queuedActions);
- }
- if (cachedMessage[0] != null) {
- if (exception instanceof UntrustedIdentityException) {
- final var address = ((UntrustedIdentityException) exception).getSender();
- final var recipientId = resolveRecipient(address);
- if (!envelope.hasSourceUuid()) {
- try {
- cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
- } catch (IOException ioException) {
- logger.warn("Failed to move cached message to recipient folder: {}",
- ioException.getMessage());
- }
- }
- } else {
- cachedMessage[0].delete();
- }
- }
- }
- handleQueuedActions(queuedActions);
- }
+ List getIdentities();
- public boolean hasCaughtUpWithOldMessages() {
- return hasCaughtUpWithOldMessages;
- }
+ List getIdentities(RecipientIdentifier.Single recipient);
- private void handleQueuedActions(final Collection queuedActions) {
- var interrupted = false;
- for (var action : queuedActions) {
- try {
- action.execute(context);
- } catch (Throwable e) {
- if ((e instanceof AssertionError || e instanceof RuntimeException)
- && e.getCause() instanceof InterruptedException) {
- interrupted = true;
- continue;
- }
- logger.warn("Message action failed.", e);
- }
- }
- if (interrupted) {
- Thread.currentThread().interrupt();
- }
- }
+ boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint);
- public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
- final RecipientId recipientId;
- try {
- recipientId = resolveRecipient(recipient);
- } catch (UnregisteredUserException e) {
- return false;
- }
- return contactHelper.isContactBlocked(recipientId);
- }
+ boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber);
- public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
- return attachmentHelper.getAttachmentFile(attachmentId);
- }
+ boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber);
- public void sendContacts() throws IOException {
- syncHelper.sendContacts();
- }
+ boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient);
- public List> getContacts() {
- return account.getContactStore().getContacts();
- }
+ String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey);
- public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) {
- final RecipientId recipientId;
- try {
- recipientId = resolveRecipient(recipientIdentifier);
- } catch (UnregisteredUserException e) {
- return null;
- }
-
- final var contact = account.getContactStore().getContact(recipientId);
- if (contact != null && !Util.isEmpty(contact.getName())) {
- return contact.getName();
- }
-
- final var profile = getRecipientProfile(recipientId);
- if (profile != null) {
- return profile.getDisplayName();
- }
-
- return null;
- }
-
- public GroupInfo getGroup(GroupId groupId) {
- return groupHelper.getGroup(groupId);
- }
-
- public List getIdentities() {
- return account.getIdentityKeyStore().getIdentities();
- }
-
- public List getIdentities(RecipientIdentifier.Single recipient) {
- IdentityInfo identity;
- try {
- identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient));
- } catch (UnregisteredUserException e) {
- identity = null;
- }
- return identity == null ? List.of() : List.of(identity);
- }
-
- /**
- * Trust this the identity with this fingerprint
- *
- * @param recipient username of the identity
- * @param fingerprint Fingerprint
- */
- public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) {
- RecipientId recipientId;
- try {
- recipientId = resolveRecipient(recipient);
- } catch (UnregisteredUserException e) {
- return false;
- }
- return trustIdentity(recipientId,
- identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
- TrustLevel.TRUSTED_VERIFIED);
- }
-
- /**
- * Trust this the identity with this safety number
- *
- * @param recipient username of the identity
- * @param safetyNumber Safety number
- */
- public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) {
- RecipientId recipientId;
- try {
- recipientId = resolveRecipient(recipient);
- } catch (UnregisteredUserException e) {
- return false;
- }
- var address = resolveSignalServiceAddress(recipientId);
- return trustIdentity(recipientId,
- identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
- TrustLevel.TRUSTED_VERIFIED);
- }
-
- /**
- * Trust this the identity with this scannable safety number
- *
- * @param recipient username of the identity
- * @param safetyNumber Scannable safety number
- */
- public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) {
- RecipientId recipientId;
- try {
- recipientId = resolveRecipient(recipient);
- } 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);
- }
-
- /**
- * Trust all keys of this identity without verification
- *
- * @param recipient username of the identity
- */
- public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) {
- RecipientId recipientId;
- try {
- recipientId = resolveRecipient(recipient);
- } catch (UnregisteredUserException e) {
- return false;
- }
- return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
- }
-
- private boolean trustIdentity(
- RecipientId recipientId, Function 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;
- }
-
- 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);
- }
- }
-
- public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
- final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey);
- return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText();
- }
-
- public 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);
- }
-
- public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
- return resolveSignalServiceAddress(resolveRecipient(address));
- }
-
- public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) {
- return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid));
- }
-
- public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
- final var address = account.getRecipientStore().resolveRecipientAddress(recipientId);
- if (address.getUuid().isPresent()) {
- return address.toSignalServiceAddress();
- }
-
- // Address in recipient store doesn't have a uuid, this shouldn't happen
- // Try to retrieve the uuid from the server
- final var number = address.getNumber().get();
- try {
- return resolveSignalServiceAddress(getRegisteredUser(number));
- } catch (IOException e) {
- logger.warn("Failed to get uuid for e164 number: {}", number, e);
- // Return SignalServiceAddress with unknown UUID
- return address.toSignalServiceAddress();
- }
- }
-
- private Set resolveRecipients(Collection recipients) throws UnregisteredUserException {
- final var recipientIds = new HashSet(recipients.size());
- for (var number : recipients) {
- final var recipientId = resolveRecipient(number);
- recipientIds.add(recipientId);
- }
- return recipientIds;
- }
-
- private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException {
- if (recipient instanceof RecipientIdentifier.Uuid) {
- return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid);
- } else {
- final var number = ((RecipientIdentifier.Number) recipient).number;
- return account.getRecipientStore().resolveRecipient(number, () -> {
- try {
- return getRegisteredUser(number);
- } catch (IOException e) {
- return null;
- }
- });
- }
- }
-
- private RecipientId resolveRecipient(SignalServiceAddress address) {
- return account.getRecipientStore().resolveRecipient(address);
- }
-
- private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
- return account.getRecipientStore().resolveRecipientTrusted(address);
- }
+ SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address);
@Override
- public void close() throws IOException {
- close(true);
- }
+ void close() throws IOException;
- private void close(boolean closeAccount) throws IOException {
- executor.shutdown();
-
- dependencies.getSignalWebSocket().disconnect();
-
- if (closeAccount && account != null) {
- account.close();
- }
- account = null;
- }
-
- public interface ReceiveMessageHandler {
+ interface ReceiveMessageHandler {
void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
new file mode 100644
index 00000000..0fd1eb33
--- /dev/null
+++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
@@ -0,0 +1,1320 @@
+/*
+ Copyright (C) 2015-2021 AsamK and contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+package org.asamk.signal.manager;
+
+import org.asamk.signal.manager.actions.HandleAction;
+import org.asamk.signal.manager.api.Device;
+import org.asamk.signal.manager.api.Group;
+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.SendGroupMessageResults;
+import org.asamk.signal.manager.api.SendMessageResults;
+import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.config.ServiceConfig;
+import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
+import org.asamk.signal.manager.groups.GroupId;
+import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupLinkState;
+import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.GroupPermission;
+import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
+import org.asamk.signal.manager.groups.LastGroupAdminException;
+import org.asamk.signal.manager.groups.NotAGroupMemberException;
+import org.asamk.signal.manager.helper.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.IncomingMessageHandler;
+import org.asamk.signal.manager.helper.PinHelper;
+import org.asamk.signal.manager.helper.PreKeyHelper;
+import org.asamk.signal.manager.helper.ProfileHelper;
+import org.asamk.signal.manager.helper.SendHelper;
+import org.asamk.signal.manager.helper.StorageHelper;
+import org.asamk.signal.manager.helper.SyncHelper;
+import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
+import org.asamk.signal.manager.jobs.Context;
+import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.groups.GroupInfo;
+import org.asamk.signal.manager.storage.identities.IdentityInfo;
+import org.asamk.signal.manager.storage.messageCache.CachedMessage;
+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.asamk.signal.manager.storage.recipients.RecipientId;
+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;
+import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
+import org.whispersystems.signalservice.api.messages.SendMessageResult;
+import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
+import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
+import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
+import org.whispersystems.signalservice.api.util.DeviceNameUtil;
+import org.whispersystems.signalservice.api.util.InvalidNumberException;
+import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
+import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
+import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
+import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
+import org.whispersystems.signalservice.internal.util.Hex;
+import org.whispersystems.signalservice.internal.util.Util;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+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;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+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;
+
+public class ManagerImpl implements Manager {
+
+ private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
+
+ private final ServiceEnvironmentConfig serviceEnvironmentConfig;
+ private final SignalDependencies dependencies;
+
+ private SignalAccount account;
+
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+
+ private final ProfileHelper profileHelper;
+ private final PinHelper pinHelper;
+ private final StorageHelper storageHelper;
+ private final SendHelper sendHelper;
+ private final SyncHelper syncHelper;
+ private final AttachmentHelper attachmentHelper;
+ private final GroupHelper groupHelper;
+ private final ContactHelper contactHelper;
+ private final IncomingMessageHandler incomingMessageHandler;
+ private final PreKeyHelper preKeyHelper;
+
+ private final Context context;
+ private boolean hasCaughtUpWithOldMessages = false;
+
+ ManagerImpl(
+ SignalAccount account,
+ PathConfig pathConfig,
+ ServiceEnvironmentConfig serviceEnvironmentConfig,
+ String userAgent
+ ) {
+ this.account = account;
+ this.serviceEnvironmentConfig = serviceEnvironmentConfig;
+
+ final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ account.getDeviceId());
+ final var sessionLock = new SignalSessionLock() {
+ private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
+
+ @Override
+ public Lock acquire() {
+ LEGACY_LOCK.lock();
+ return LEGACY_LOCK::unlock;
+ }
+ };
+ this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
+ userAgent,
+ credentialsProvider,
+ account.getSignalProtocolStore(),
+ executor,
+ sessionLock);
+ final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
+ final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
+ final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
+
+ 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);
+ this.profileHelper = new ProfileHelper(account,
+ dependencies,
+ avatarStore,
+ account.getProfileStore()::getProfileKey,
+ unidentifiedAccessHelper::getAccessFor,
+ this::resolveSignalServiceAddress);
+ final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
+ this::getRecipientProfile,
+ account::getSelfRecipientId,
+ dependencies.getGroupsV2Operations(),
+ dependencies.getGroupsV2Api(),
+ this::resolveSignalServiceAddress);
+ this.sendHelper = new SendHelper(account,
+ dependencies,
+ unidentifiedAccessHelper,
+ this::resolveSignalServiceAddress,
+ account.getRecipientStore(),
+ this::handleIdentityFailure,
+ this::getGroupInfo,
+ this::refreshRegisteredUser);
+ this.groupHelper = new GroupHelper(account,
+ dependencies,
+ attachmentHelper,
+ sendHelper,
+ groupV2Helper,
+ avatarStore,
+ this::resolveSignalServiceAddress,
+ account.getRecipientStore());
+ this.storageHelper = new StorageHelper(account, dependencies, groupHelper, profileHelper);
+ this.contactHelper = new ContactHelper(account);
+ this.syncHelper = new SyncHelper(account,
+ attachmentHelper,
+ sendHelper,
+ groupHelper,
+ avatarStore,
+ this::resolveSignalServiceAddress);
+ preKeyHelper = new PreKeyHelper(account, dependencies);
+
+ this.context = new Context(account,
+ dependencies,
+ stickerPackStore,
+ sendHelper,
+ groupHelper,
+ syncHelper,
+ profileHelper,
+ storageHelper,
+ preKeyHelper);
+ var jobExecutor = new JobExecutor(context);
+
+ this.incomingMessageHandler = new IncomingMessageHandler(account,
+ dependencies,
+ account.getRecipientStore(),
+ this::resolveSignalServiceAddress,
+ groupHelper,
+ contactHelper,
+ attachmentHelper,
+ syncHelper,
+ this::getRecipientProfile,
+ jobExecutor);
+ }
+
+ @Override
+ public String getSelfNumber() {
+ return account.getUsername();
+ }
+
+ @Override
+ public void checkAccountState() throws IOException {
+ if (account.getLastReceiveTimestamp() == 0) {
+ logger.info("The Signal protocol expects that incoming messages are regularly received.");
+ } else {
+ var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
+ long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
+ if (days > 7) {
+ logger.warn(
+ "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
+ days);
+ }
+ }
+ preKeyHelper.refreshPreKeysIfNecessary();
+ if (account.getUuid() == null) {
+ account.setUuid(dependencies.getAccountManager().getOwnUuid());
+ }
+ updateAccountAttributes(null);
+ }
+
+ /**
+ * This is used for checking a set of phone numbers for registration on Signal
+ *
+ * @param numbers The set of phone number in question
+ * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
+ * @throws IOException if its unable to get the contacts to check if they're registered
+ */
+ @Override
+ public Map> areUsersRegistered(Set numbers) throws IOException {
+ Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
+ try {
+ return PhoneNumberFormatter.formatNumber(n, account.getUsername());
+ } catch (InvalidNumberException e) {
+ return "";
+ }
+ }));
+
+ // Note "registeredUsers" has no optionals. It only gives us info on users who are registered
+ var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values()
+ .stream()
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet()));
+
+ return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
+ final var number = canonicalizedNumbers.get(n);
+ final var uuid = registeredUsers.get(number);
+ return new Pair<>(number.isEmpty() ? null : number, uuid);
+ }));
+ }
+
+ @Override
+ public void updateAccountAttributes(String deviceName) throws IOException {
+ final String encryptedDeviceName;
+ if (deviceName == null) {
+ encryptedDeviceName = account.getEncryptedDeviceName();
+ } else {
+ final var privateKey = account.getIdentityKeyPair().getPrivateKey();
+ encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
+ account.setEncryptedDeviceName(encryptedDeviceName);
+ }
+ dependencies.getAccountManager()
+ .setAccountAttributes(encryptedDeviceName,
+ null,
+ account.getLocalRegistrationId(),
+ true,
+ null,
+ account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
+ account.getSelfUnidentifiedAccessKey(),
+ account.isUnrestrictedUnidentifiedAccess(),
+ capabilities,
+ account.isDiscoverableByPhoneNumber());
+ }
+
+ @Override
+ public void updateConfiguration(
+ final Boolean readReceipts,
+ final Boolean unidentifiedDeliveryIndicators,
+ final Boolean typingIndicators,
+ final Boolean linkPreviews
+ ) throws IOException, NotMasterDeviceException {
+ if (!account.isMasterDevice()) {
+ throw new NotMasterDeviceException();
+ }
+
+ final var configurationStore = account.getConfigurationStore();
+ if (readReceipts != null) {
+ configurationStore.setReadReceipts(readReceipts);
+ }
+ if (unidentifiedDeliveryIndicators != null) {
+ configurationStore.setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators);
+ }
+ if (typingIndicators != null) {
+ configurationStore.setTypingIndicators(typingIndicators);
+ }
+ if (linkPreviews != null) {
+ configurationStore.setLinkPreviews(linkPreviews);
+ }
+ syncHelper.sendConfigurationMessage();
+ }
+
+ /**
+ * @param givenName if null, the previous givenName will be kept
+ * @param familyName if null, the previous familyName will be kept
+ * @param about if null, the previous about text will be kept
+ * @param aboutEmoji if null, the previous about emoji will be kept
+ * @param avatar if avatar is null the image from the local avatar store is used (if present),
+ */
+ @Override
+ public void setProfile(
+ String givenName, final String familyName, String about, String aboutEmoji, Optional avatar
+ ) throws IOException {
+ profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar);
+ syncHelper.sendSyncFetchProfileMessage();
+ }
+
+ @Override
+ public void unregister() throws IOException {
+ // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
+ // If this is the master device, other users can't send messages to this number anymore.
+ // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
+ dependencies.getAccountManager().setGcmId(Optional.absent());
+
+ account.setRegistered(false);
+ }
+
+ @Override
+ public void deleteAccount() throws IOException {
+ try {
+ pinHelper.removeRegistrationLockPin();
+ } catch (UnauthenticatedResponseException e) {
+ logger.warn("Failed to remove registration lock pin");
+ }
+ account.setRegistrationLockPin(null, null);
+
+ dependencies.getAccountManager().deleteAccount();
+
+ account.setRegistered(false);
+ }
+
+ @Override
+ public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
+ dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
+ }
+
+ @Override
+ public List getLinkedDevices() throws IOException {
+ var devices = dependencies.getAccountManager().getDevices();
+ account.setMultiDevice(devices.size() > 1);
+ var identityKey = account.getIdentityKeyPair().getPrivateKey();
+ return devices.stream().map(d -> {
+ String deviceName = d.getName();
+ if (deviceName != null) {
+ try {
+ deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
+ } catch (IOException e) {
+ logger.debug("Failed to decrypt device name, maybe plain text?", e);
+ }
+ }
+ return new Device(d.getId(),
+ deviceName,
+ d.getCreated(),
+ d.getLastSeen(),
+ d.getId() == account.getDeviceId());
+ }).collect(Collectors.toList());
+ }
+
+ @Override
+ public void removeLinkedDevices(long deviceId) throws IOException {
+ dependencies.getAccountManager().removeDevice(deviceId);
+ var devices = dependencies.getAccountManager().getDevices();
+ account.setMultiDevice(devices.size() > 1);
+ }
+
+ @Override
+ public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
+ var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
+
+ addDevice(info.deviceIdentifier, info.deviceKey);
+ }
+
+ private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
+ var identityKeyPair = account.getIdentityKeyPair();
+ var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
+
+ dependencies.getAccountManager()
+ .addDevice(deviceIdentifier,
+ deviceKey,
+ identityKeyPair,
+ Optional.of(account.getProfileKey().serialize()),
+ verificationCode);
+ account.setMultiDevice(true);
+ }
+
+ @Override
+ public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException {
+ if (!account.isMasterDevice()) {
+ throw new RuntimeException("Only master device can set a PIN");
+ }
+ if (pin.isPresent()) {
+ final var masterKey = account.getPinMasterKey() != null
+ ? account.getPinMasterKey()
+ : KeyUtils.createMasterKey();
+
+ pinHelper.setRegistrationLockPin(pin.get(), masterKey);
+
+ account.setRegistrationLockPin(pin.get(), masterKey);
+ } else {
+ // Remove KBS Pin
+ pinHelper.removeRegistrationLockPin();
+
+ account.setRegistrationLockPin(null, null);
+ }
+ }
+
+ void refreshPreKeys() throws IOException {
+ preKeyHelper.refreshPreKeys();
+ }
+
+ @Override
+ public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException {
+ return profileHelper.getRecipientProfile(resolveRecipient(recipient));
+ }
+
+ private Profile getRecipientProfile(RecipientId recipientId) {
+ return profileHelper.getRecipientProfile(recipientId);
+ }
+
+ @Override
+ public List getGroups() {
+ return account.getGroupStore().getGroups().stream().map(this::toGroup).collect(Collectors.toList());
+ }
+
+ private Group toGroup(final GroupInfo groupInfo) {
+ if (groupInfo == null) {
+ return null;
+ }
+
+ return new Group(groupInfo.getGroupId(),
+ groupInfo.getTitle(),
+ groupInfo.getDescription(),
+ groupInfo.getGroupInviteLink(),
+ groupInfo.getMembers()
+ .stream()
+ .map(account.getRecipientStore()::resolveRecipientAddress)
+ .collect(Collectors.toSet()),
+ groupInfo.getPendingMembers()
+ .stream()
+ .map(account.getRecipientStore()::resolveRecipientAddress)
+ .collect(Collectors.toSet()),
+ groupInfo.getRequestingMembers()
+ .stream()
+ .map(account.getRecipientStore()::resolveRecipientAddress)
+ .collect(Collectors.toSet()),
+ groupInfo.getAdminMembers()
+ .stream()
+ .map(account.getRecipientStore()::resolveRecipientAddress)
+ .collect(Collectors.toSet()),
+ groupInfo.isBlocked(),
+ groupInfo.getMessageExpirationTime(),
+ groupInfo.isAnnouncementGroup(),
+ groupInfo.isMember(account.getSelfRecipientId()));
+ }
+
+ @Override
+ public SendGroupMessageResults quitGroup(
+ GroupId groupId, Set groupAdmins
+ ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
+ final var newAdmins = resolveRecipients(groupAdmins);
+ return groupHelper.quitGroup(groupId, newAdmins);
+ }
+
+ @Override
+ public void deleteGroup(GroupId groupId) throws IOException {
+ groupHelper.deleteGroup(groupId);
+ }
+
+ @Override
+ public Pair createGroup(
+ String name, Set members, File avatarFile
+ ) throws IOException, AttachmentInvalidException {
+ return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile);
+ }
+
+ @Override
+ public SendGroupMessageResults updateGroup(
+ GroupId groupId,
+ String name,
+ String description,
+ Set members,
+ Set removeMembers,
+ Set admins,
+ Set removeAdmins,
+ boolean resetGroupLink,
+ GroupLinkState groupLinkState,
+ GroupPermission addMemberPermission,
+ GroupPermission editDetailsPermission,
+ File avatarFile,
+ Integer expirationTimer,
+ Boolean isAnnouncementGroup
+ ) 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);
+ }
+
+ @Override
+ public Pair joinGroup(
+ GroupInviteLinkUrl inviteLinkUrl
+ ) throws IOException, GroupLinkNotActiveException {
+ return groupHelper.joinGroup(inviteLinkUrl);
+ }
+
+ private SendMessageResults sendMessage(
+ SignalServiceDataMessage.Builder messageBuilder, Set recipients
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ var results = new HashMap>();
+ long timestamp = System.currentTimeMillis();
+ messageBuilder.withTimestamp(timestamp);
+ for (final var recipient : recipients) {
+ if (recipient instanceof RecipientIdentifier.Single) {
+ final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient);
+ final var result = sendHelper.sendMessage(messageBuilder, recipientId);
+ results.put(recipient, List.of(result));
+ } else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
+ final var result = sendHelper.sendSelfMessage(messageBuilder);
+ results.put(recipient, List.of(result));
+ } else if (recipient instanceof RecipientIdentifier.Group) {
+ final var groupId = ((RecipientIdentifier.Group) recipient).groupId;
+ final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId);
+ results.put(recipient, result);
+ }
+ }
+ return new SendMessageResults(timestamp, results);
+ }
+
+ private void sendTypingMessage(
+ SignalServiceTypingMessage.Action action, Set recipients
+ ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ final var timestamp = System.currentTimeMillis();
+ for (var recipient : recipients) {
+ if (recipient instanceof RecipientIdentifier.Single) {
+ final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent());
+ final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient);
+ sendHelper.sendTypingMessage(message, recipientId);
+ } else if (recipient instanceof RecipientIdentifier.Group) {
+ final var groupId = ((RecipientIdentifier.Group) recipient).groupId;
+ final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize()));
+ sendHelper.sendGroupTypingMessage(message, groupId);
+ }
+ }
+ }
+
+ @Override
+ public void sendTypingMessage(
+ TypingAction action, Set recipients
+ ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ sendTypingMessage(action.toSignalService(), recipients);
+ }
+
+ @Override
+ public void sendReadReceipt(
+ RecipientIdentifier.Single sender, List messageIds
+ ) throws IOException, UntrustedIdentityException {
+ var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
+ messageIds,
+ System.currentTimeMillis());
+
+ sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
+ }
+
+ @Override
+ public void sendViewedReceipt(
+ RecipientIdentifier.Single sender, List messageIds
+ ) throws IOException, UntrustedIdentityException {
+ var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
+ messageIds,
+ System.currentTimeMillis());
+
+ sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
+ }
+
+ @Override
+ public SendMessageResults sendMessage(
+ Message message, Set recipients
+ ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ final var messageBuilder = SignalServiceDataMessage.newBuilder();
+ applyMessage(messageBuilder, message);
+ return sendMessage(messageBuilder, recipients);
+ }
+
+ private void applyMessage(
+ final SignalServiceDataMessage.Builder messageBuilder, final Message message
+ ) throws AttachmentInvalidException, IOException {
+ messageBuilder.withBody(message.getMessageText());
+ final var attachments = message.getAttachments();
+ if (attachments != null) {
+ messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments));
+ }
+ }
+
+ @Override
+ public SendMessageResults sendRemoteDeleteMessage(
+ long targetSentTimestamp, Set recipients
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
+ final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
+ return sendMessage(messageBuilder, recipients);
+ }
+
+ @Override
+ public SendMessageResults sendMessageReaction(
+ String emoji,
+ boolean remove,
+ RecipientIdentifier.Single targetAuthor,
+ long targetSentTimestamp,
+ Set recipients
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ var targetAuthorRecipientId = resolveRecipient(targetAuthor);
+ var reaction = new SignalServiceDataMessage.Reaction(emoji,
+ remove,
+ resolveSignalServiceAddress(targetAuthorRecipientId),
+ targetSentTimestamp);
+ final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction);
+ return sendMessage(messageBuilder, recipients);
+ }
+
+ @Override
+ public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException {
+ var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
+
+ try {
+ return sendMessage(messageBuilder,
+ recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()));
+ } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
+ throw new AssertionError(e);
+ } finally {
+ for (var recipient : recipients) {
+ final var recipientId = resolveRecipient(recipient);
+ account.getSessionStore().deleteAllSessions(recipientId);
+ }
+ }
+ }
+
+ @Override
+ public void setContactName(
+ RecipientIdentifier.Single recipient, String name
+ ) throws NotMasterDeviceException, UnregisteredUserException {
+ if (!account.isMasterDevice()) {
+ throw new NotMasterDeviceException();
+ }
+ contactHelper.setContactName(resolveRecipient(recipient), name);
+ }
+
+ @Override
+ public void setContactBlocked(
+ RecipientIdentifier.Single recipient, boolean blocked
+ ) throws NotMasterDeviceException, IOException {
+ if (!account.isMasterDevice()) {
+ throw new NotMasterDeviceException();
+ }
+ contactHelper.setContactBlocked(resolveRecipient(recipient), blocked);
+ // TODO cycle our profile key
+ syncHelper.sendBlockedList();
+ }
+
+ @Override
+ public void setGroupBlocked(
+ final GroupId groupId, final boolean blocked
+ ) throws GroupNotFoundException, IOException {
+ groupHelper.setGroupBlocked(groupId, blocked);
+ // TODO cycle our profile key
+ syncHelper.sendBlockedList();
+ }
+
+ /**
+ * Change the expiration timer for a contact
+ */
+ @Override
+ public void setExpirationTimer(
+ RecipientIdentifier.Single recipient, int messageExpirationTimer
+ ) throws IOException {
+ var recipientId = resolveRecipient(recipient);
+ contactHelper.setExpirationTimer(recipientId, messageExpirationTimer);
+ final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
+ try {
+ sendMessage(messageBuilder, Set.of(recipient));
+ } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Upload the sticker pack from path.
+ *
+ * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
+ * @return if successful, returns the URL to install the sticker pack in the signal app
+ */
+ @Override
+ public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
+ var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
+
+ var messageSender = dependencies.getMessageSender();
+
+ var packKey = KeyUtils.createStickerUploadKey();
+ var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
+ var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
+
+ var sticker = new Sticker(packId, packKey);
+ account.getStickerStore().updateSticker(sticker);
+
+ try {
+ return new URI("https",
+ "signal.art",
+ "/addstickers/",
+ "pack_id="
+ + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8)
+ + "&pack_key="
+ + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8));
+ } catch (URISyntaxException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ public void requestAllSyncData() throws IOException {
+ syncHelper.requestAllSyncData();
+ retrieveRemoteStorage();
+ }
+
+ void retrieveRemoteStorage() throws IOException {
+ if (account.getStorageKey() != null) {
+ storageHelper.readDataFromStorage();
+ }
+ }
+
+ private byte[] getSenderCertificate() {
+ byte[] certificate;
+ try {
+ if (account.isPhoneNumberShared()) {
+ certificate = dependencies.getAccountManager().getSenderCertificate();
+ } else {
+ certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
+ }
+ } catch (IOException e) {
+ logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
+ return null;
+ }
+ // TODO cache for a day
+ return certificate;
+ }
+
+ private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException {
+ final var address = resolveSignalServiceAddress(recipientId);
+ if (!address.getNumber().isPresent()) {
+ return recipientId;
+ }
+ final var number = address.getNumber().get();
+ final var uuid = getRegisteredUser(number);
+ return resolveRecipientTrusted(new SignalServiceAddress(uuid, number));
+ }
+
+ private UUID getRegisteredUser(final String number) throws IOException {
+ final Map uuidMap;
+ try {
+ uuidMap = getRegisteredUsers(Set.of(number));
+ } catch (NumberFormatException e) {
+ throw new UnregisteredUserException(number, e);
+ }
+ final var uuid = uuidMap.get(number);
+ if (uuid == null) {
+ throw new UnregisteredUserException(number, null);
+ }
+ return uuid;
+ }
+
+ private Map getRegisteredUsers(final Set numbers) throws IOException {
+ final Map registeredUsers;
+ try {
+ registeredUsers = dependencies.getAccountManager()
+ .getRegisteredUsers(ServiceConfig.getIasKeyStore(),
+ numbers,
+ serviceEnvironmentConfig.getCdsMrenclave());
+ } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) {
+ throw new IOException(e);
+ }
+
+ // Store numbers as recipients so we have the number/uuid association
+ registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number)));
+
+ return registeredUsers;
+ }
+
+ private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
+ Set queuedActions = new HashSet<>();
+ for (var cachedMessage : account.getMessageCache().getCachedMessages()) {
+ var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage);
+ if (actions != null) {
+ queuedActions.addAll(actions);
+ }
+ }
+ handleQueuedActions(queuedActions);
+ }
+
+ private List retryFailedReceivedMessage(
+ final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage
+ ) {
+ var envelope = cachedMessage.loadEnvelope();
+ if (envelope == null) {
+ cachedMessage.delete();
+ return null;
+ }
+
+ final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler);
+ final var actions = result.first();
+ final var exception = result.second();
+
+ if (exception instanceof UntrustedIdentityException) {
+ if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) {
+ // Envelope is more than a month old, cleaning up.
+ cachedMessage.delete();
+ return null;
+ }
+ if (!envelope.hasSourceUuid()) {
+ final var identifier = ((UntrustedIdentityException) exception).getSender();
+ final var recipientId = account.getRecipientStore().resolveRecipient(identifier);
+ try {
+ account.getMessageCache().replaceSender(cachedMessage, recipientId);
+ } catch (IOException ioException) {
+ logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage());
+ }
+ }
+ return null;
+ }
+
+ // If successful and for all other errors that are not recoverable, delete the cached message
+ cachedMessage.delete();
+ return actions;
+ }
+
+ @Override
+ public void receiveMessages(
+ long timeout,
+ TimeUnit unit,
+ boolean returnOnTimeout,
+ boolean ignoreAttachments,
+ ReceiveMessageHandler handler
+ ) throws IOException {
+ retryFailedReceivedMessages(handler, ignoreAttachments);
+
+ Set queuedActions = new HashSet<>();
+
+ final var signalWebSocket = dependencies.getSignalWebSocket();
+ signalWebSocket.connect();
+
+ hasCaughtUpWithOldMessages = false;
+
+ while (!Thread.interrupted()) {
+ SignalServiceEnvelope envelope;
+ final CachedMessage[] cachedMessage = {null};
+ account.setLastReceiveTimestamp(System.currentTimeMillis());
+ logger.debug("Checking for new message from server");
+ try {
+ var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> {
+ final var recipientId = envelope1.hasSourceUuid()
+ ? resolveRecipient(envelope1.getSourceAddress())
+ : null;
+ // store message on disk, before acknowledging receipt to the server
+ cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
+ });
+ if (result.isPresent()) {
+ envelope = result.get();
+ logger.debug("New message received from server");
+ } else {
+ logger.debug("Received indicator that server queue is empty");
+ handleQueuedActions(queuedActions);
+ queuedActions.clear();
+
+ hasCaughtUpWithOldMessages = true;
+ synchronized (this) {
+ this.notifyAll();
+ }
+
+ // Continue to wait another timeout for new messages
+ continue;
+ }
+ } catch (AssertionError e) {
+ if (e.getCause() instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ break;
+ } else {
+ throw e;
+ }
+ } catch (WebSocketUnavailableException e) {
+ logger.debug("Pipe unexpectedly unavailable, connecting");
+ signalWebSocket.connect();
+ continue;
+ } catch (TimeoutException e) {
+ if (returnOnTimeout) return;
+ continue;
+ }
+
+ final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler);
+ queuedActions.addAll(result.first());
+ final var exception = result.second();
+
+ if (hasCaughtUpWithOldMessages) {
+ handleQueuedActions(queuedActions);
+ }
+ if (cachedMessage[0] != null) {
+ if (exception instanceof UntrustedIdentityException) {
+ final var address = ((UntrustedIdentityException) exception).getSender();
+ final var recipientId = resolveRecipient(address);
+ if (!envelope.hasSourceUuid()) {
+ try {
+ cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
+ } catch (IOException ioException) {
+ logger.warn("Failed to move cached message to recipient folder: {}",
+ ioException.getMessage());
+ }
+ }
+ } else {
+ cachedMessage[0].delete();
+ }
+ }
+ }
+ handleQueuedActions(queuedActions);
+ }
+
+ @Override
+ public boolean hasCaughtUpWithOldMessages() {
+ return hasCaughtUpWithOldMessages;
+ }
+
+ private void handleQueuedActions(final Collection queuedActions) {
+ var interrupted = false;
+ for (var action : queuedActions) {
+ try {
+ action.execute(context);
+ } catch (Throwable e) {
+ if ((e instanceof AssertionError || e instanceof RuntimeException)
+ && e.getCause() instanceof InterruptedException) {
+ interrupted = true;
+ continue;
+ }
+ logger.warn("Message action failed.", e);
+ }
+ }
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
+ final RecipientId recipientId;
+ try {
+ recipientId = resolveRecipient(recipient);
+ } catch (UnregisteredUserException e) {
+ return false;
+ }
+ return contactHelper.isContactBlocked(recipientId);
+ }
+
+ @Override
+ public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
+ return attachmentHelper.getAttachmentFile(attachmentId);
+ }
+
+ @Override
+ public void sendContacts() throws IOException {
+ syncHelper.sendContacts();
+ }
+
+ @Override
+ public List> getContacts() {
+ return account.getContactStore()
+ .getContacts()
+ .stream()
+ .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second()))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public String getContactOrProfileName(RecipientIdentifier.Single recipient) {
+ final RecipientId recipientId;
+ try {
+ recipientId = resolveRecipient(recipient);
+ } catch (UnregisteredUserException e) {
+ return null;
+ }
+
+ final var contact = account.getContactStore().getContact(recipientId);
+ if (contact != null && !Util.isEmpty(contact.getName())) {
+ return contact.getName();
+ }
+
+ final var profile = getRecipientProfile(recipientId);
+ if (profile != null) {
+ return profile.getDisplayName();
+ }
+
+ return null;
+ }
+
+ @Override
+ public Group getGroup(GroupId groupId) {
+ return toGroup(groupHelper.getGroup(groupId));
+ }
+
+ public GroupInfo getGroupInfo(GroupId groupId) {
+ return groupHelper.getGroup(groupId);
+ }
+
+ @Override
+ public List getIdentities() {
+ return account.getIdentityKeyStore()
+ .getIdentities()
+ .stream()
+ .map(this::toIdentity)
+ .collect(Collectors.toList());
+ }
+
+ private Identity toIdentity(final IdentityInfo identityInfo) {
+ if (identityInfo == null) {
+ return null;
+ }
+
+ final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId());
+ return new Identity(address,
+ identityInfo.getIdentityKey(),
+ computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()),
+ computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()),
+ identityInfo.getTrustLevel(),
+ identityInfo.getDateAdded());
+ }
+
+ @Override
+ public List getIdentities(RecipientIdentifier.Single recipient) {
+ IdentityInfo identity;
+ try {
+ identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient));
+ } catch (UnregisteredUserException e) {
+ identity = null;
+ }
+ return identity == null ? List.of() : List.of(toIdentity(identity));
+ }
+
+ /**
+ * Trust this the identity with this fingerprint
+ *
+ * @param recipient username of the identity
+ * @param fingerprint Fingerprint
+ */
+ @Override
+ public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) {
+ RecipientId recipientId;
+ try {
+ recipientId = resolveRecipient(recipient);
+ } catch (UnregisteredUserException e) {
+ return false;
+ }
+ return trustIdentity(recipientId,
+ identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
+ TrustLevel.TRUSTED_VERIFIED);
+ }
+
+ /**
+ * Trust this the identity with this safety number
+ *
+ * @param recipient username of the identity
+ * @param safetyNumber Safety number
+ */
+ @Override
+ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) {
+ RecipientId recipientId;
+ try {
+ recipientId = resolveRecipient(recipient);
+ } catch (UnregisteredUserException e) {
+ return false;
+ }
+ var address = resolveSignalServiceAddress(recipientId);
+ return trustIdentity(recipientId,
+ identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
+ TrustLevel.TRUSTED_VERIFIED);
+ }
+
+ /**
+ * Trust this the identity with this scannable safety number
+ *
+ * @param recipient username of the identity
+ * @param safetyNumber Scannable safety number
+ */
+ @Override
+ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) {
+ RecipientId recipientId;
+ try {
+ recipientId = resolveRecipient(recipient);
+ } 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);
+ }
+
+ /**
+ * Trust all keys of this identity without verification
+ *
+ * @param recipient username of the identity
+ */
+ @Override
+ public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) {
+ RecipientId recipientId;
+ try {
+ recipientId = resolveRecipient(recipient);
+ } catch (UnregisteredUserException e) {
+ return false;
+ }
+ return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
+ }
+
+ private boolean trustIdentity(
+ RecipientId recipientId, Function 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;
+ }
+
+ 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);
+ }
+
+ @Override
+ public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
+ return resolveSignalServiceAddress(resolveRecipient(address));
+ }
+
+ private SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
+ final var address = account.getRecipientStore().resolveRecipientAddress(recipientId);
+ if (address.getUuid().isPresent()) {
+ return address.toSignalServiceAddress();
+ }
+
+ // Address in recipient store doesn't have a uuid, this shouldn't happen
+ // Try to retrieve the uuid from the server
+ final var number = address.getNumber().get();
+ final UUID uuid;
+ try {
+ uuid = getRegisteredUser(number);
+ } catch (IOException e) {
+ logger.warn("Failed to get uuid for e164 number: {}", number, e);
+ // Return SignalServiceAddress with unknown UUID
+ return address.toSignalServiceAddress();
+ }
+ return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid));
+ }
+
+ private Set resolveRecipients(Collection recipients) throws UnregisteredUserException {
+ final var recipientIds = new HashSet(recipients.size());
+ for (var number : recipients) {
+ final var recipientId = resolveRecipient(number);
+ recipientIds.add(recipientId);
+ }
+ return recipientIds;
+ }
+
+ private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException {
+ if (recipient instanceof RecipientIdentifier.Uuid) {
+ return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid);
+ } else {
+ final var number = ((RecipientIdentifier.Number) recipient).number;
+ return account.getRecipientStore().resolveRecipient(number, () -> {
+ try {
+ return getRegisteredUser(number);
+ } catch (IOException e) {
+ return null;
+ }
+ });
+ }
+ }
+
+ private RecipientId resolveRecipient(SignalServiceAddress address) {
+ return account.getRecipientStore().resolveRecipient(address);
+ }
+
+ private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
+ return account.getRecipientStore().resolveRecipientTrusted(address);
+ }
+
+ @Override
+ public void close() throws IOException {
+ close(true);
+ }
+
+ private void close(boolean closeAccount) throws IOException {
+ executor.shutdown();
+
+ dependencies.getSignalWebSocket().disconnect();
+
+ if (closeAccount && account != null) {
+ account.close();
+ }
+ account = null;
+ }
+
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
index 90dc6c66..226de9be 100644
--- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
+++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
@@ -126,9 +126,9 @@ public class ProvisioningManager {
profileKey,
TrustNewIdentity.ON_FIRST_USE);
- Manager m = null;
+ ManagerImpl m = null;
try {
- m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent);
+ m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
logger.debug("Refreshing pre keys");
try {
@@ -178,7 +178,7 @@ public class ProvisioningManager {
return false;
}
- final var m = new Manager(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
+ final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
try (m) {
m.checkAccountState();
} catch (AuthorizationFailedException ignored) {
diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java
index 443a7969..c42782f7 100644
--- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java
+++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java
@@ -91,18 +91,18 @@ public class RegistrationManager implements Closeable {
}
public static RegistrationManager init(
- String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
+ String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
) throws IOException {
var pathConfig = PathConfig.createDefault(settingsPath);
final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
- if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
+ if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) {
var identityKey = KeyUtils.generateIdentityKeyPair();
var registrationId = KeyHelper.generateRegistrationId(false);
var profileKey = KeyUtils.createProfileKey();
var account = SignalAccount.create(pathConfig.getDataPath(),
- username,
+ number,
identityKey,
registrationId,
profileKey,
@@ -111,7 +111,7 @@ public class RegistrationManager implements Closeable {
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
}
- var account = SignalAccount.load(pathConfig.getDataPath(), username, true, TrustNewIdentity.ON_FIRST_USE);
+ var account = SignalAccount.load(pathConfig.getDataPath(), number, true, TrustNewIdentity.ON_FIRST_USE);
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
}
@@ -177,21 +177,21 @@ public class RegistrationManager implements Closeable {
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin);
- Manager m = null;
+ ManagerImpl m = null;
try {
- m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent);
+ m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
account = null;
m.refreshPreKeys();
+ if (response.isStorageCapable()) {
+ m.retrieveRemoteStorage();
+ }
// Set an initial empty profile so user can be added to groups
try {
m.setProfile(null, null, null, null, null);
} catch (NoClassDefFoundError e) {
logger.warn("Failed to set default profile: {}", e.getMessage());
}
- if (response.isStorageCapable()) {
- m.retrieveRemoteStorage();
- }
final var result = m;
m = null;
diff --git a/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java b/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java
index d506f0c6..905392c5 100644
--- a/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java
+++ b/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java
@@ -4,16 +4,16 @@ import java.io.File;
public class UserAlreadyExists extends Exception {
- private final String username;
+ private final String number;
private final File fileName;
- public UserAlreadyExists(String username, File fileName) {
- this.username = username;
+ public UserAlreadyExists(String number, File fileName) {
+ this.number = number;
this.fileName = fileName;
}
- public String getUsername() {
- return username;
+ public String getNumber() {
+ return number;
}
public File getFileName() {
diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java
new file mode 100644
index 00000000..0e050f0a
--- /dev/null
+++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java
@@ -0,0 +1,20 @@
+package org.asamk.signal.manager.actions;
+
+import org.asamk.signal.manager.jobs.Context;
+
+public class SendSyncConfigurationAction implements HandleAction {
+
+ private static final SendSyncConfigurationAction INSTANCE = new SendSyncConfigurationAction();
+
+ private SendSyncConfigurationAction() {
+ }
+
+ public static SendSyncConfigurationAction create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void execute(Context context) throws Throwable {
+ context.getSyncHelper().sendConfigurationMessage();
+ }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Device.java b/lib/src/main/java/org/asamk/signal/manager/api/Device.java
index 76074cbf..9ee0d36a 100644
--- a/lib/src/main/java/org/asamk/signal/manager/api/Device.java
+++ b/lib/src/main/java/org/asamk/signal/manager/api/Device.java
@@ -6,12 +6,14 @@ public class Device {
private final String name;
private final long created;
private final long lastSeen;
+ private final boolean thisDevice;
- public Device(long id, String name, long created, long lastSeen) {
+ public Device(long id, String name, long created, long lastSeen, final boolean thisDevice) {
this.id = id;
this.name = name;
this.created = created;
this.lastSeen = lastSeen;
+ this.thisDevice = thisDevice;
}
public long getId() {
@@ -29,4 +31,8 @@ public class Device {
public long getLastSeen() {
return lastSeen;
}
+
+ public boolean isThisDevice() {
+ return thisDevice;
+ }
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Group.java b/lib/src/main/java/org/asamk/signal/manager/api/Group.java
new file mode 100644
index 00000000..650e10b6
--- /dev/null
+++ b/lib/src/main/java/org/asamk/signal/manager/api/Group.java
@@ -0,0 +1,99 @@
+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.storage.recipients.RecipientAddress;
+
+import java.util.Set;
+
+public class Group {
+
+ private final GroupId groupId;
+ private final String title;
+ private final String description;
+ private final GroupInviteLinkUrl groupInviteLinkUrl;
+ private final Set members;
+ private final Set pendingMembers;
+ private final Set requestingMembers;
+ private final Set adminMembers;
+ private final boolean isBlocked;
+ private final int messageExpirationTime;
+ private final boolean isAnnouncementGroup;
+ private final boolean isMember;
+
+ public Group(
+ final GroupId groupId,
+ final String title,
+ final String description,
+ final GroupInviteLinkUrl groupInviteLinkUrl,
+ final Set members,
+ final Set pendingMembers,
+ final Set requestingMembers,
+ final Set adminMembers,
+ final boolean isBlocked,
+ final int messageExpirationTime,
+ final boolean isAnnouncementGroup,
+ final boolean isMember
+ ) {
+ this.groupId = groupId;
+ this.title = title;
+ this.description = description;
+ this.groupInviteLinkUrl = groupInviteLinkUrl;
+ this.members = members;
+ this.pendingMembers = pendingMembers;
+ this.requestingMembers = requestingMembers;
+ this.adminMembers = adminMembers;
+ this.isBlocked = isBlocked;
+ this.messageExpirationTime = messageExpirationTime;
+ this.isAnnouncementGroup = isAnnouncementGroup;
+ this.isMember = isMember;
+ }
+
+ public GroupId getGroupId() {
+ return groupId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public GroupInviteLinkUrl getGroupInviteLinkUrl() {
+ return groupInviteLinkUrl;
+ }
+
+ public Set getMembers() {
+ return members;
+ }
+
+ public Set getPendingMembers() {
+ return pendingMembers;
+ }
+
+ public Set getRequestingMembers() {
+ return requestingMembers;
+ }
+
+ public Set getAdminMembers() {
+ return adminMembers;
+ }
+
+ public boolean isBlocked() {
+ return isBlocked;
+ }
+
+ public int getMessageExpirationTime() {
+ return messageExpirationTime;
+ }
+
+ public boolean isAnnouncementGroup() {
+ return isAnnouncementGroup;
+ }
+
+ public boolean isMember() {
+ return isMember;
+ }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Identity.java b/lib/src/main/java/org/asamk/signal/manager/api/Identity.java
new file mode 100644
index 00000000..4f6f21f6
--- /dev/null
+++ b/lib/src/main/java/org/asamk/signal/manager/api/Identity.java
@@ -0,0 +1,65 @@
+package org.asamk.signal.manager.api;
+
+import org.asamk.signal.manager.TrustLevel;
+import org.asamk.signal.manager.storage.recipients.RecipientAddress;
+import org.whispersystems.libsignal.IdentityKey;
+
+import java.util.Date;
+
+public class Identity {
+
+ private final RecipientAddress recipient;
+ private final IdentityKey identityKey;
+ private final String safetyNumber;
+ private final byte[] scannableSafetyNumber;
+ private final TrustLevel trustLevel;
+ private final Date dateAdded;
+
+ public Identity(
+ final RecipientAddress recipient,
+ final IdentityKey identityKey,
+ final String safetyNumber,
+ final byte[] scannableSafetyNumber,
+ final TrustLevel trustLevel,
+ final Date dateAdded
+ ) {
+ this.recipient = recipient;
+ this.identityKey = identityKey;
+ this.safetyNumber = safetyNumber;
+ this.scannableSafetyNumber = scannableSafetyNumber;
+ this.trustLevel = trustLevel;
+ this.dateAdded = dateAdded;
+ }
+
+ public RecipientAddress getRecipient() {
+ return recipient;
+ }
+
+ public IdentityKey getIdentityKey() {
+ return this.identityKey;
+ }
+
+ public TrustLevel getTrustLevel() {
+ return this.trustLevel;
+ }
+
+ boolean isTrusted() {
+ return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED;
+ }
+
+ public Date getDateAdded() {
+ return this.dateAdded;
+ }
+
+ public byte[] getFingerprint() {
+ return identityKey.getPublicKey().serialize();
+ }
+
+ public String getSafetyNumber() {
+ return safetyNumber;
+ }
+
+ public byte[] getScannableSafetyNumber() {
+ return scannableSafetyNumber;
+ }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java
index 4a66cbb3..be1029e6 100644
--- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java
+++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java
@@ -1,6 +1,7 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupId;
+import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
@@ -12,14 +13,9 @@ public abstract class RecipientIdentifier {
public static class NoteToSelf extends RecipientIdentifier {
- @Override
- public boolean equals(final Object obj) {
- return obj instanceof NoteToSelf;
- }
+ public static NoteToSelf INSTANCE = new NoteToSelf();
- @Override
- public int hashCode() {
- return 5;
+ private NoteToSelf() {
}
}
@@ -34,6 +30,17 @@ public abstract class RecipientIdentifier {
public static Single fromAddress(SignalServiceAddress address) {
return new Uuid(address.getUuid());
}
+
+ public static Single fromAddress(RecipientAddress address) {
+ if (address.getNumber().isPresent()) {
+ return new Number(address.getNumber().get());
+ } else if (address.getUuid().isPresent()) {
+ return new Uuid(address.getUuid().get());
+ }
+ throw new AssertionError("RecipientAddress without identifier");
+ }
+
+ public abstract String getIdentifier();
}
public static class Uuid extends Single {
@@ -58,6 +65,11 @@ public abstract class RecipientIdentifier {
public int hashCode() {
return uuid.hashCode();
}
+
+ @Override
+ public String getIdentifier() {
+ return uuid.toString();
+ }
}
public static class Number extends Single {
@@ -82,6 +94,11 @@ public abstract class RecipientIdentifier {
public int hashCode() {
return number.hashCode();
}
+
+ @Override
+ public String getIdentifier() {
+ return number;
+ }
}
public static class Group extends RecipientIdentifier {
diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java
index 7762a4cb..177f6697 100644
--- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java
+++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java
@@ -7,6 +7,7 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
+import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@@ -38,6 +39,7 @@ class LiveConfig {
private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api.directory.signal.org";
private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org";
private final static String STORAGE_URL = "https://storage.signal.org";
+ private final static String SIGNAL_CDSH_URL = "";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private final static Optional dns = Optional.absent();
@@ -58,6 +60,7 @@ class LiveConfig {
TRUST_STORE)},
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
+ new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)},
interceptors,
dns,
proxy,
diff --git a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java
index bedec52c..d643f10a 100644
--- a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java
+++ b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java
@@ -7,6 +7,7 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
+import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@@ -38,6 +39,7 @@ class SandboxConfig {
private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api-staging.directory.signal.org";
private final static String SIGNAL_KEY_BACKUP_URL = "https://api-staging.backup.signal.org";
private final static String STORAGE_URL = "https://storage-staging.signal.org";
+ private final static String SIGNAL_CDSH_URL = "https://cdsh.staging.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private final static Optional dns = Optional.absent();
@@ -58,6 +60,7 @@ class SandboxConfig {
TRUST_STORE)},
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
+ new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)},
interceptors,
dns,
proxy,
diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java
index 3677bba1..a9a08d93 100644
--- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java
+++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java
@@ -39,7 +39,13 @@ public class ServiceConfig {
logger.warn("Failed to call libzkgroup: {}", e.getMessage());
zkGroupAvailable = false;
}
- capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true);
+ capabilities = new AccountAttributes.Capabilities(false,
+ zkGroupAvailable,
+ false,
+ zkGroupAvailable,
+ true,
+ true,
+ false);
try {
TrustStore contactTrustStore = new IasTrustStore();
diff --git a/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java b/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java
new file mode 100644
index 00000000..e7e1b5f5
--- /dev/null
+++ b/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java
@@ -0,0 +1,93 @@
+package org.asamk.signal.manager.configuration;
+
+public class ConfigurationStore {
+
+ private final Saver saver;
+
+ private Boolean readReceipts;
+ private Boolean unidentifiedDeliveryIndicators;
+ private Boolean typingIndicators;
+ private Boolean linkPreviews;
+
+ public ConfigurationStore(final Saver saver) {
+ this.saver = saver;
+ }
+
+ public static ConfigurationStore fromStorage(Storage storage, Saver saver) {
+ final var store = new ConfigurationStore(saver);
+ store.readReceipts = storage.readReceipts;
+ store.unidentifiedDeliveryIndicators = storage.unidentifiedDeliveryIndicators;
+ store.typingIndicators = storage.typingIndicators;
+ store.linkPreviews = storage.linkPreviews;
+ return store;
+ }
+
+ public Boolean getReadReceipts() {
+ return readReceipts;
+ }
+
+ public void setReadReceipts(final boolean readReceipts) {
+ this.readReceipts = readReceipts;
+ saver.save(toStorage());
+ }
+
+ public Boolean getUnidentifiedDeliveryIndicators() {
+ return unidentifiedDeliveryIndicators;
+ }
+
+ public void setUnidentifiedDeliveryIndicators(final boolean unidentifiedDeliveryIndicators) {
+ this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
+ saver.save(toStorage());
+ }
+
+ public Boolean getTypingIndicators() {
+ return typingIndicators;
+ }
+
+ public void setTypingIndicators(final boolean typingIndicators) {
+ this.typingIndicators = typingIndicators;
+ saver.save(toStorage());
+ }
+
+ public Boolean getLinkPreviews() {
+ return linkPreviews;
+ }
+
+ public void setLinkPreviews(final boolean linkPreviews) {
+ this.linkPreviews = linkPreviews;
+ saver.save(toStorage());
+ }
+
+ private Storage toStorage() {
+ return new Storage(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews);
+ }
+
+ public static final class Storage {
+
+ public Boolean readReceipts;
+ public Boolean unidentifiedDeliveryIndicators;
+ public Boolean typingIndicators;
+ public Boolean linkPreviews;
+
+ // For deserialization
+ private Storage() {
+ }
+
+ public Storage(
+ final Boolean readReceipts,
+ final Boolean unidentifiedDeliveryIndicators,
+ final Boolean typingIndicators,
+ final Boolean linkPreviews
+ ) {
+ this.readReceipts = readReceipts;
+ this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
+ this.typingIndicators = typingIndicators;
+ this.linkPreviews = linkPreviews;
+ }
+ }
+
+ public interface Saver {
+
+ void save(Storage storage);
+ }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
index 3ddd6edd..62f4f111 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
@@ -145,7 +145,10 @@ public class GroupHelper {
groupMasterKey);
}
if (group == null) {
- group = groupV2Helper.getDecryptedGroup(groupSecretParams);
+ try {
+ group = groupV2Helper.getDecryptedGroup(groupSecretParams);
+ } catch (NotAGroupMemberException ignored) {
+ }
}
if (group != null) {
storeProfileKeysFromMembers(group);
@@ -348,10 +351,20 @@ public class GroupHelper {
private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
final var group = account.getGroupStore().getGroup(groupId);
- if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) {
- final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
- ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), recipientResolver);
- account.getGroupStore().updateGroup(group);
+ if (group instanceof GroupInfoV2) {
+ final var groupInfoV2 = (GroupInfoV2) group;
+ if (forceUpdate || (!groupInfoV2.isPermissionDenied() && groupInfoV2.getGroup() == null)) {
+ final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+ DecryptedGroup decryptedGroup;
+ try {
+ decryptedGroup = groupV2Helper.getDecryptedGroup(groupSecretParams);
+ } catch (NotAGroupMemberException e) {
+ groupInfoV2.setPermissionDenied(true);
+ decryptedGroup = null;
+ }
+ groupInfoV2.setGroup(decryptedGroup, recipientResolver);
+ account.getGroupStore().updateGroup(group);
+ }
}
return group;
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
index 3187fca1..746af2f9 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
@@ -6,6 +6,7 @@ import org.asamk.signal.manager.groups.GroupLinkPassword;
import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupUtils;
+import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientId;
@@ -35,6 +36,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.File;
@@ -78,10 +80,16 @@ public class GroupV2Helper {
this.addressResolver = addressResolver;
}
- public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
+ public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException {
try {
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
+ } catch (NonSuccessfulResponseCodeException e) {
+ if (e.getCode() == 403) {
+ throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
+ }
+ logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
+ return null;
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
return null;
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
index 0917a214..16f47d3c 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
@@ -15,6 +15,7 @@ import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
import org.asamk.signal.manager.actions.SendReceiptAction;
import org.asamk.signal.manager.actions.SendRetryMessageRequestAction;
import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
+import org.asamk.signal.manager.actions.SendSyncConfigurationAction;
import org.asamk.signal.manager.actions.SendSyncContactsAction;
import org.asamk.signal.manager.actions.SendSyncGroupsAction;
import org.asamk.signal.manager.actions.SendSyncKeysAction;
@@ -144,7 +145,8 @@ public final class IncomingMessageHandler {
final var sender = account.getRecipientStore().resolveRecipient(e.getSender());
final var senderProfile = profileProvider.getProfile(sender);
final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId());
- if (senderProfile != null
+ if (e.getSenderDevice() != account.getDeviceId()
+ && senderProfile != null
&& senderProfile.getCapabilities().contains(Profile.Capability.senderKey)
&& selfProfile != null
&& selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
@@ -270,7 +272,9 @@ public final class IncomingMessageHandler {
if (rm.isKeysRequest()) {
actions.add(SendSyncKeysAction.create());
}
- // TODO Handle rm.isConfigurationRequest();
+ if (rm.isConfigurationRequest()) {
+ actions.add(SendSyncConfigurationAction.create());
+ }
}
if (syncMessage.getGroups().isPresent()) {
logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring.");
@@ -352,7 +356,13 @@ public final class IncomingMessageHandler {
}
}
if (syncMessage.getConfiguration().isPresent()) {
- // TODO
+ final var configurationMessage = syncMessage.getConfiguration().get();
+ final var configurationStore = account.getConfigurationStore();
+ configurationStore.setReadReceipts(configurationMessage.getReadReceipts().orNull());
+ configurationStore.setLinkPreviews(configurationMessage.getLinkPreviews().orNull());
+ configurationStore.setTypingIndicators(configurationMessage.getTypingIndicators().orNull());
+ configurationStore.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators()
+ .orNull());
}
return actions;
}
@@ -415,7 +425,7 @@ public final class IncomingMessageHandler {
}
final var recipientId = recipientResolver.resolveRecipient(source);
- if (!group.isMember(recipientId)) {
+ if (!group.isMember(recipientId) && !(group.isPendingMember(recipientId) && message.isGroupV2Update())) {
return true;
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
index d4f8ae5d..e24d41fa 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
@@ -32,6 +32,8 @@ import java.nio.file.Files;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
import java.util.Set;
import io.reactivex.rxjava3.core.Single;
@@ -109,6 +111,17 @@ public final class ProfileHelper {
*/
public void setProfile(
String givenName, final String familyName, String about, String aboutEmoji, Optional avatar
+ ) throws IOException {
+ setProfile(true, givenName, familyName, about, aboutEmoji, avatar);
+ }
+
+ public void setProfile(
+ boolean uploadProfile,
+ String givenName,
+ final String familyName,
+ String about,
+ String aboutEmoji,
+ Optional avatar
) throws IOException {
var profile = getRecipientProfile(account.getSelfRecipientId());
var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
@@ -126,17 +139,22 @@ public final class ProfileHelper {
}
var newProfile = builder.build();
- try (final var streamDetails = avatar == null
- ? avatarStore.retrieveProfileAvatar(account.getSelfAddress())
- : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
- dependencies.getAccountManager()
- .setVersionedProfile(account.getUuid(),
- account.getProfileKey(),
- newProfile.getInternalServiceName(),
- newProfile.getAbout() == null ? "" : newProfile.getAbout(),
- newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
- Optional.absent(),
- streamDetails);
+ if (uploadProfile) {
+ try (final var streamDetails = avatar == null
+ ? avatarStore.retrieveProfileAvatar(account.getSelfAddress())
+ : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
+ final var avatarPath = dependencies.getAccountManager()
+ .setVersionedProfile(account.getUuid(),
+ account.getProfileKey(),
+ newProfile.getInternalServiceName(),
+ newProfile.getAbout() == null ? "" : newProfile.getAbout(),
+ newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
+ Optional.absent(),
+ streamDetails,
+ List.of(/* TODO */));
+ builder.withAvatarUrlPath(avatarPath.orNull());
+ newProfile = builder.build();
+ }
}
if (avatar != null) {
@@ -195,6 +213,7 @@ public final class ProfileHelper {
null,
null,
null,
+ null,
ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
ProfileUtils.getCapabilities(encryptedProfile));
}
@@ -240,15 +259,23 @@ public final class ProfileHelper {
private Profile decryptProfileAndDownloadAvatar(
final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) {
- if (encryptedProfile.getAvatar() != null) {
- downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId),
- encryptedProfile.getAvatar(),
- profileKey);
- }
+ final var avatarPath = encryptedProfile.getAvatar();
+ downloadProfileAvatar(recipientId, avatarPath, profileKey);
return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
}
+ public void downloadProfileAvatar(
+ final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey
+ ) {
+ var profile = account.getProfileStore().getProfile(recipientId);
+ if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) {
+ downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId), avatarPath, profileKey);
+ var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
+ account.getProfileStore().storeProfile(recipientId, builder.withAvatarUrlPath(avatarPath).build());
+ }
+ }
+
private ProfileAndCredential retrieveProfileSync(
RecipientId recipientId, SignalServiceProfile.RequestType requestType
) throws IOException {
@@ -308,6 +335,15 @@ public final class ProfileHelper {
private void downloadProfileAvatar(
SignalServiceAddress address, String avatarPath, ProfileKey profileKey
) {
+ if (avatarPath == null) {
+ try {
+ avatarStore.deleteProfileAvatar(address);
+ } catch (IOException e) {
+ logger.warn("Failed to delete local profile avatar, ignoring: {}", e.getMessage());
+ }
+ return;
+ }
+
try {
avatarStore.storeProfileAvatar(address,
outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
index 4caab519..b68e65b4 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
@@ -32,13 +32,18 @@ public class StorageHelper {
private final SignalAccount account;
private final SignalDependencies dependencies;
private final GroupHelper groupHelper;
+ private final ProfileHelper profileHelper;
public StorageHelper(
- final SignalAccount account, final SignalDependencies dependencies, final GroupHelper groupHelper
+ final SignalAccount account,
+ final SignalDependencies dependencies,
+ final GroupHelper groupHelper,
+ final ProfileHelper profileHelper
) {
this.account = account;
this.dependencies = dependencies;
this.groupHelper = groupHelper;
+ this.profileHelper = profileHelper;
}
public void readDataFromStorage() throws IOException {
@@ -188,13 +193,37 @@ public class StorageHelper {
return;
}
+ if (!accountRecord.getE164().equals(account.getUsername())) {
+ // TODO implement changed number handling
+ }
+
+ account.getConfigurationStore().setReadReceipts(accountRecord.isReadReceiptsEnabled());
+ account.getConfigurationStore().setTypingIndicators(accountRecord.isTypingIndicatorsEnabled());
+ account.getConfigurationStore()
+ .setUnidentifiedDeliveryIndicators(accountRecord.isSealedSenderIndicatorsEnabled());
+ account.getConfigurationStore().setLinkPreviews(accountRecord.isLinkPreviewsEnabled());
+
if (accountRecord.getProfileKey().isPresent()) {
+ ProfileKey profileKey;
try {
- account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get()));
+ profileKey = new ProfileKey(accountRecord.getProfileKey().get());
} catch (InvalidInputException e) {
logger.warn("Received invalid profile key from storage");
+ profileKey = null;
+ }
+ if (profileKey != null) {
+ account.setProfileKey(profileKey);
+ final var avatarPath = accountRecord.getAvatarUrlPath().orNull();
+ profileHelper.downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, profileKey);
}
}
+
+ profileHelper.setProfile(false,
+ accountRecord.getGivenName().orNull(),
+ accountRecord.getFamilyName().orNull(),
+ null,
+ null,
+ null);
}
private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException {
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
index 6db1ca7d..e3fc7fc2 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
@@ -14,6 +14,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
@@ -221,6 +222,15 @@ public class SyncHelper {
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
+ public void sendConfigurationMessage() throws IOException {
+ final var config = account.getConfigurationStore();
+ var configurationMessage = new ConfigurationMessage(Optional.fromNullable(config.getReadReceipts()),
+ Optional.fromNullable(config.getUnidentifiedDeliveryIndicators()),
+ Optional.fromNullable(config.getTypingIndicators()),
+ Optional.fromNullable(config.getLinkPreviews()));
+ sendHelper.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
+ }
+
public void handleSyncDeviceContacts(final InputStream input) throws IOException {
final var s = new DeviceContactsInputStream(input);
DeviceContact c;
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
index fd4ec597..5bb9fdeb 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.TrustLevel;
+import org.asamk.signal.manager.configuration.ConfigurationStore;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
@@ -103,6 +104,8 @@ public class SignalAccount implements Closeable {
private RecipientStore recipientStore;
private StickerStore stickerStore;
private StickerStore.Storage stickerStoreStorage;
+ private ConfigurationStore configurationStore;
+ private ConfigurationStore.Storage configurationStoreStorage;
private MessageCache messageCache;
@@ -159,6 +162,7 @@ public class SignalAccount implements Closeable {
account.recipientStore,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore);
+ account.configurationStore = new ConfigurationStore(account::saveConfigurationStore);
account.registered = false;
@@ -267,6 +271,7 @@ public class SignalAccount implements Closeable {
account.recipientStore,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore);
+ account.configurationStore = new ConfigurationStore(account::saveConfigurationStore);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.migrateLegacyConfigs();
@@ -491,6 +496,15 @@ public class SignalAccount implements Closeable {
stickerStore = new StickerStore(this::saveStickerStore);
}
+ if (rootNode.hasNonNull("configurationStore")) {
+ configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
+ ConfigurationStore.Storage.class);
+ configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage,
+ this::saveConfigurationStore);
+ } else {
+ configurationStore = new ConfigurationStore(this::saveConfigurationStore);
+ }
+
migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
if (migratedLegacyConfig) {
@@ -617,6 +631,7 @@ public class SignalAccount implements Closeable {
profile.getFamilyName(),
profile.getAbout(),
profile.getAboutEmoji(),
+ null,
profile.isUnrestrictedUnidentifiedAccess()
? Profile.UnidentifiedAccessMode.UNRESTRICTED
: profile.getUnidentifiedAccess() != null
@@ -677,6 +692,11 @@ public class SignalAccount implements Closeable {
save();
}
+ private void saveConfigurationStore(ConfigurationStore.Storage storage) {
+ this.configurationStoreStorage = storage;
+ save();
+ }
+
private void save() {
synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode();
@@ -707,7 +727,8 @@ public class SignalAccount implements Closeable {
profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
.putPOJO("groupStore", groupStoreStorage)
- .putPOJO("stickerStore", stickerStoreStorage);
+ .putPOJO("stickerStore", stickerStoreStorage)
+ .putPOJO("configurationStore", configurationStoreStorage);
try {
try (var output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
@@ -797,6 +818,10 @@ public class SignalAccount implements Closeable {
return senderKeyStore;
}
+ public ConfigurationStore getConfigurationStore() {
+ return configurationStore;
+ }
+
public MessageCache getMessageCache() {
return messageCache;
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java
index f86dcb04..a06b83df 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java
@@ -22,16 +22,23 @@ public class GroupInfoV2 extends GroupInfo {
private boolean blocked;
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
private RecipientResolver recipientResolver;
+ private boolean permissionDenied;
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
this.groupId = groupId;
this.masterKey = masterKey;
}
- public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) {
+ public GroupInfoV2(
+ final GroupIdV2 groupId,
+ final GroupMasterKey masterKey,
+ final boolean blocked,
+ final boolean permissionDenied
+ ) {
this.groupId = groupId;
this.masterKey = masterKey;
this.blocked = blocked;
+ this.permissionDenied = permissionDenied;
}
@Override
@@ -44,6 +51,9 @@ public class GroupInfoV2 extends GroupInfo {
}
public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
+ if (group != null) {
+ this.permissionDenied = false;
+ }
this.group = group;
this.recipientResolver = recipientResolver;
}
@@ -151,4 +161,12 @@ public class GroupInfoV2 extends GroupInfo {
public boolean isAnnouncementGroup() {
return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED;
}
+
+ public void setPermissionDenied(final boolean permissionDenied) {
+ this.permissionDenied = permissionDenied;
+ }
+
+ public boolean isPermissionDenied() {
+ return permissionDenied;
+ }
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
index 4adc413a..fe8f85a6 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
@@ -104,7 +104,7 @@ public class GroupStore {
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
}
- return new GroupInfoV2(groupId, masterKey, g2.blocked);
+ return new GroupInfoV2(groupId, masterKey, g2.blocked, g2.permissionDenied);
}).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
return new GroupStore(groupCachePath, groups, recipientResolver, saver);
@@ -268,13 +268,13 @@ public class GroupStore {
final var g2 = (GroupInfoV2) g;
return new Storage.GroupV2(g2.getGroupId().toBase64(),
Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
- g2.isBlocked());
+ g2.isBlocked(),
+ g2.isPermissionDenied());
}).collect(Collectors.toList()));
}
public static class Storage {
- // @JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class)
public List groups;
@@ -408,46 +408,24 @@ public class GroupStore {
public String groupId;
public String masterKey;
public boolean blocked;
+ public boolean permissionDenied;
// For deserialization
private GroupV2() {
}
- public GroupV2(final String groupId, final String masterKey, final boolean blocked) {
+ public GroupV2(
+ final String groupId, final String masterKey, final boolean blocked, final boolean permissionDenied
+ ) {
this.groupId = groupId;
this.masterKey = masterKey;
this.blocked = blocked;
+ this.permissionDenied = permissionDenied;
}
}
}
- // private static class GroupsSerializer extends JsonSerializer> {
-//
-// @Override
-// public void serialize(
-// final List groups, final JsonGenerator jgen, final SerializerProvider provider
-// ) throws IOException {
-// jgen.writeStartArray(groups.size());
-// for (var group : groups) {
-// if (group instanceof GroupInfoV1) {
-// jgen.writeObject(group);
-// } else if (group instanceof GroupInfoV2) {
-// final var groupV2 = (GroupInfoV2) group;
-// jgen.writeStartObject();
-// jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
-// jgen.writeStringField("masterKey",
-// Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
-// jgen.writeBooleanField("blocked", groupV2.isBlocked());
-// jgen.writeEndObject();
-// } else {
-// throw new AssertionError("Unknown group version");
-// }
-// }
-// jgen.writeEndArray();
-// }
-// }
-//
private static class GroupsDeserializer extends JsonDeserializer> {
@Override
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java
index d61a81b5..c6ba5c92 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java
@@ -17,6 +17,8 @@ public class Profile {
private final String aboutEmoji;
+ private final String avatarUrlPath;
+
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final Set capabilities;
@@ -27,6 +29,7 @@ public class Profile {
final String familyName,
final String about,
final String aboutEmoji,
+ final String avatarUrlPath,
final UnidentifiedAccessMode unidentifiedAccessMode,
final Set capabilities
) {
@@ -35,6 +38,7 @@ public class Profile {
this.familyName = familyName;
this.about = about;
this.aboutEmoji = aboutEmoji;
+ this.avatarUrlPath = avatarUrlPath;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities;
}
@@ -45,6 +49,7 @@ public class Profile {
familyName = builder.familyName;
about = builder.about;
aboutEmoji = builder.aboutEmoji;
+ avatarUrlPath = builder.avatarUrlPath;
unidentifiedAccessMode = builder.unidentifiedAccessMode;
capabilities = builder.capabilities;
}
@@ -60,6 +65,7 @@ public class Profile {
builder.familyName = copy.getFamilyName();
builder.about = copy.getAbout();
builder.aboutEmoji = copy.getAboutEmoji();
+ builder.avatarUrlPath = copy.getAvatarUrlPath();
builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode();
builder.capabilities = copy.getCapabilities();
return builder;
@@ -107,6 +113,10 @@ public class Profile {
return aboutEmoji;
}
+ public String getAvatarUrlPath() {
+ return avatarUrlPath;
+ }
+
public UnidentifiedAccessMode getUnidentifiedAccessMode() {
return unidentifiedAccessMode;
}
@@ -152,6 +162,7 @@ public class Profile {
private String familyName;
private String about;
private String aboutEmoji;
+ private String avatarUrlPath;
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
private Set capabilities = Collections.emptySet();
private long lastUpdateTimestamp = 0;
@@ -179,6 +190,11 @@ public class Profile {
return this;
}
+ public Builder withAvatarUrlPath(final String val) {
+ avatarUrlPath = val;
+ return this;
+ }
+
public Builder withUnidentifiedAccessMode(final UnidentifiedAccessMode val) {
unidentifiedAccessMode = val;
return this;
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java
index 88877d83..c0f5b0b8 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java
@@ -57,6 +57,16 @@ public class RecipientAddress {
}
}
+ public String getLegacyIdentifier() {
+ if (e164.isPresent()) {
+ return e164.get();
+ } else if (uuid.isPresent()) {
+ return uuid.get().toString();
+ } else {
+ throw new AssertionError("Given the checks in the constructor, this should not be possible.");
+ }
+ }
+
public boolean matches(RecipientAddress other) {
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) || (
e164.isPresent() && other.e164.isPresent() && e164.get().equals(other.e164.get())
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
index bace6a6b..16302692 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
@@ -89,6 +89,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
r.profile.familyName,
r.profile.about,
r.profile.aboutEmoji,
+ r.profile.avatarUrlPath,
Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
r.profile.capabilities.stream()
.map(Profile.Capability::valueOfOrNull)
@@ -445,6 +446,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
recipient.getProfile().getFamilyName(),
recipient.getProfile().getAbout(),
recipient.getProfile().getAboutEmoji(),
+ recipient.getProfile().getAvatarUrlPath(),
recipient.getProfile().getUnidentifiedAccessMode().name(),
recipient.getProfile()
.getCapabilities()
@@ -558,6 +560,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
public String familyName;
public String about;
public String aboutEmoji;
+ public String avatarUrlPath;
public String unidentifiedAccessMode;
public Set capabilities;
@@ -571,6 +574,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
final String familyName,
final String about,
final String aboutEmoji,
+ final String avatarUrlPath,
final String unidentifiedAccessMode,
final Set capabilities
) {
@@ -579,6 +583,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
this.familyName = familyName;
this.about = about;
this.aboutEmoji = aboutEmoji;
+ this.avatarUrlPath = avatarUrlPath;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities;
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
index 7ceb07f6..c1b1183c 100644
--- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
+++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
@@ -27,6 +27,7 @@ public class ProfileUtils {
nameParts.second(),
about,
aboutEmoji,
+ encryptedProfile.getAvatar(),
getUnidentifiedAccessMode(encryptedProfile, profileCipher),
getCapabilities(encryptedProfile));
} catch (InvalidCiphertextException e) {
diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc
index 545f3676..9eb0a90c 100755
--- a/man/signal-cli-dbus.5.adoc
+++ b/man/signal-cli-dbus.5.adoc
@@ -44,6 +44,64 @@ Phone numbers always have the format +
== 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 (_).
+Only `version()` is activated in single-user mode; the rest are disabled.
+
+link() -> deviceLinkUri::
+link(newDeviceName) -> deviceLinkUri::
+* 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
+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
+
+listAccounts() -> accountList::
+* accountList : Array of all attached accounts in DBus object path form
+
+Exceptions: None
+
+register(number, voiceVerification) -> <>::
+* number : Phone number
+* voiceVerification : true = use voice verification; false = use SMS verification
+
+Exceptions: Failure, InvalidNumber, RequiresCaptcha
+
+registerWithCaptcha(number, voiceVerification, captcha) -> <>::
+* number : Phone number
+* voiceVerification : true = use voice verification; false = use SMS verification
+* captcha : Captcha string
+
+Exceptions: Failure, InvalidNumber, RequiresCaptcha
+
+verify(number, verificationCode) -> <>::
+* number : Phone number
+* verificationCode : Code received from Signal after successful registration request
+
+Command fails if PIN was set after previous registration; use verifyWithPin instead.
+
+Exception: Failure, InvalidNumber
+
+verifyWithPin(number, verificationCode, pin) -> <>::
+* 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
+
+version() -> version::
+* version : Version string of signal-cli
+
+Exceptions: None
+
+=== Other methods
+
updateGroup(groupId, newName, members, avatar) -> groupId::
* groupId : Byte array representing the internal group identifier
* newName : New name of group (empty if unchanged)
@@ -52,8 +110,11 @@ updateGroup(groupId, newName, members, avatar) -> groupId::
Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound
-updateProfile(newName, about , aboutEmoji , avatar, remove) -> <>::
-* newName : New name for your own profile (empty if unchanged)
+updateProfile(name, about, aboutEmoji , avatar, remove) -> <>::
+updateProfile(givenName, familyName, about, aboutEmoji , avatar, remove) -> <>::
+* name : Name for your own profile (empty if unchanged)
+* givenName : Given name for your own profile (empty if unchanged)
+* familyName : Family name for your own profile (empty if unchanged)
* about : About message for profile (empty if unchanged)
* aboutEmoji : Emoji for profile (empty if unchanged)
* avatar : Filename of avatar picture for profile (empty if unchanged)
@@ -61,6 +122,12 @@ updateProfile(newName, about , aboutEmoji , avatar, remove) -> <>
Exceptions: Failure
+setExpirationTimer(number, expiration) -> <>::
+* 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, block) -> <>::
* number : Phone number affected by method
* block : false=remove block , true=block
@@ -107,6 +174,18 @@ sendGroupMessage(message, attachments, groupId) -> timestamp::
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, attachments) -> timestamp::
* 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)
@@ -229,11 +308,59 @@ isGroupBlocked(groupId) -> state::
Exceptions: None; for unknown groups false is returned
+removePin() -> <>::
+
+Removes registration PIN protection.
+
+Exception: Failure
+
+setPin(pin) -> <>::
+* 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::
* version : Version string of signal-cli
-isRegistred -> result::
-* result : Currently always returns true
+isRegistered() -> result::
+isRegistered(number) -> result::
+isRegistered(numbers) -> results::
+* 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) -> <>::
+* deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app
+
+Exception: InvalidUri
+
+listDevices() -> devices::
+* devices : String array of linked devices
+
+Exception: Failure
+
+removeDevice(deviceId) -> <>::
+* deviceId : Device ID to remove, obtained from listDevices() command
+
+Exception: Failure
+
+updateDeviceName(deviceName) -> <>::
+* deviceName : New name
+
+Set a new name for this device (main or linked).
+
+Exception: Failure
+
+uploadStickerPack(stickerPackPath) -> url::
+* 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
== Signals
diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc
index 573ade7c..9829fe00 100644
--- a/man/signal-cli.1.adoc
+++ b/man/signal-cli.1.adoc
@@ -113,6 +113,23 @@ Can fix problems with receiving messages.
*-n* NAME, *--device-name* NAME::
Set a new device name for the main or linked device
+=== updateConfiguration
+
+Update signal configs and sync them to linked devices.
+This command only works on the main devices.
+
+*--read-receipts* {true,false}::
+Indicates if Signal should send read receipts.
+
+*--unidentified-delivery-indicators* {true,false}::
+Indicates if Signal should show unidentified delivery indicators.
+
+*--typing-indicators* {true,false}::
+Indicates if Signal should send/show typing indicators.
+
+*--link-previews* {true,false}::
+Indicates if Signal should generate link previews.
+
=== setPin
Set a registration lock pin, to prevent others from registering this number.
diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java
index e0f67888..65ed3d21 100644
--- a/src/main/java/org/asamk/Signal.java
+++ b/src/main/java/org/asamk/Signal.java
@@ -2,9 +2,12 @@ package org.asamk;
import org.asamk.signal.dbus.DbusAttachment;
import org.asamk.signal.dbus.DbusMention;
+import org.freedesktop.dbus.DBusPath;
+import org.freedesktop.dbus.annotations.DBusProperty;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.freedesktop.dbus.interfaces.DBusInterface;
+import org.freedesktop.dbus.interfaces.Properties;
import org.freedesktop.dbus.messages.DBusSignal;
import java.util.ArrayList;
@@ -17,6 +20,8 @@ import java.util.Map;
*/
public interface Signal extends DBusInterface {
+ String getSelfNumber();
+
long sendMessage(
String message, List attachmentNames, String recipient
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity;
@@ -30,7 +35,7 @@ public interface Signal extends DBusInterface {
) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity;
void sendReadReceipt(
- String recipient, List targetSentTimestamp
+ String recipient, List messageIds
) throws Error.Failure, Error.UntrustedIdentity;
long sendRemoteDeleteMessage(
@@ -53,6 +58,10 @@ public interface Signal extends DBusInterface {
String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients
) throws Error.InvalidNumber, Error.Failure;
+ void sendContacts() throws Error.Failure;
+
+ void sendSyncRequest() throws Error.Failure;
+
long sendNoteToSelfMessage(
String message, List attachmentNames
) throws Error.AttachmentInvalid, Error.Failure;
@@ -71,6 +80,8 @@ public interface Signal extends DBusInterface {
void setContactName(String number, String name) throws Error.InvalidNumber;
+ void setExpirationTimer(final String number, final int expiration) throws Error.Failure;
+
void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber;
void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId;
@@ -87,10 +98,37 @@ public interface Signal extends DBusInterface {
byte[] groupId, String name, List members, String avatar
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId;
+ boolean isRegistered() throws Error.Failure, Error.InvalidNumber;
+
+ boolean isRegistered(String number) throws Error.Failure, Error.InvalidNumber;
+
+ List isRegistered(List numbers) throws Error.Failure, Error.InvalidNumber;
+
+ void addDevice(String uri) throws Error.InvalidUri;
+
+ DBusPath getDevice(long deviceId);
+
+ List listDevices() throws Error.Failure;
+
+ DBusPath getThisDevice();
+
+ void updateProfile(
+ String givenName,
+ String familyName,
+ String about,
+ String aboutEmoji,
+ String avatarPath,
+ boolean removeAvatar
+ ) throws Error.Failure;
+
void updateProfile(
String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar
) throws Error.Failure;
+ void removePin();
+
+ void setPin(String registrationLockPin);
+
String version();
List listNumbers();
@@ -107,6 +145,8 @@ public interface Signal extends DBusInterface {
byte[] joinGroup(final String groupLink) throws Error.Failure;
+ String uploadStickerPack(String stickerPackPath) throws Error.Failure;
+
class MessageReceived extends DBusSignal {
private final long timestamp;
@@ -379,6 +419,15 @@ public interface Signal extends DBusInterface {
}
}
+ @DBusProperty(name = "Id", type = Integer.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)
+ interface Device extends DBusInterface, Properties {
+
+ void removeDevice() throws Error.Failure;
+ }
+
interface Error {
class AttachmentInvalid extends DBusExecutionException {
@@ -388,6 +437,13 @@ public interface Signal extends DBusInterface {
}
}
+ class InvalidUri extends DBusExecutionException {
+
+ public InvalidUri(final String message) {
+ super(message);
+ }
+ }
+
class Failure extends DBusExecutionException {
public Failure(final String message) {
diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java
index 4aa510d6..3d35ff8f 100644
--- a/src/main/java/org/asamk/signal/App.java
+++ b/src/main/java/org/asamk/signal/App.java
@@ -8,7 +8,6 @@ import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.Commands;
-import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.commands.MultiLocalCommand;
@@ -19,6 +18,7 @@ import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
+import org.asamk.signal.dbus.DbusManagerImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotRegisteredException;
import org.asamk.signal.manager.ProvisioningManager;
@@ -29,6 +29,7 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.util.IOUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
+import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
@@ -116,8 +117,8 @@ public class App {
var username = ns.getString("username");
- final var useDbus = ns.getBoolean("dbus");
- final var useDbusSystem = ns.getBoolean("dbus-system");
+ final var useDbus = Boolean.TRUE.equals(ns.getBoolean("dbus"));
+ final var useDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
if (useDbus || useDbusSystem) {
// If username is null, it will connect to the default object path
initDbusClient(command, username, useDbusSystem, outputWriter);
@@ -161,7 +162,7 @@ public class App {
}
if (username == null) {
- var usernames = Manager.getAllLocalUsernames(dataPath);
+ var usernames = Manager.getAllLocalNumbers(dataPath);
if (command instanceof MultiLocalCommand) {
handleMultiLocalCommand((MultiLocalCommand) command,
@@ -346,8 +347,14 @@ public class App {
) throws CommandException {
if (command instanceof ExtendedDbusCommand) {
((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn, outputWriter);
- } else if (command instanceof DbusCommand) {
- ((DbusCommand) command).handleCommand(ns, ts, outputWriter);
+ } else if (command instanceof LocalCommand) {
+ try {
+ ((LocalCommand) command).handleCommand(ns, new DbusManagerImpl(ts, dBusConn), outputWriter);
+ } catch (UnsupportedOperationException e) {
+ throw new UserErrorException("Command is not yet implemented via dbus", e);
+ } catch (DBusExecutionException e) {
+ throw new UnexpectedErrorException(e.getMessage(), e);
+ }
} else {
throw new UserErrorException("Command is not yet implemented via dbus");
}
diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java
index 26079ec6..2a95e6de 100644
--- a/src/main/java/org/asamk/signal/Main.java
+++ b/src/main/java/org/asamk/signal/Main.java
@@ -80,7 +80,7 @@ public class Main {
return false;
}
- return ns.getBoolean("verbose");
+ return Boolean.TRUE.equals(ns.getBoolean("verbose"));
}
private static void configureLogging(final boolean verbose) {
diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
index bc9244f8..35790678 100644
--- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
@@ -61,13 +61,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final var recipientName = getLegacyIdentifier(m.resolveSignalServiceAddress(e.getSender()));
writer.println(
"Use 'signal-cli -u {} listIdentities -n {}', verify the key and run 'signal-cli -u {} trust -v \"FINGER_PRINT\" {}' to mark it as trusted",
- m.getUsername(),
+ m.getSelfNumber(),
recipientName,
- m.getUsername(),
+ m.getSelfNumber(),
recipientName);
writer.println(
"If you don't care about security, use 'signal-cli -u {} trust -a {}' to trust it without verification",
- m.getUsername(),
+ m.getSelfNumber(),
recipientName);
} else {
writer.println("Exception: {} ({})", exception.getMessage(), exception.getClass().getSimpleName());
@@ -657,7 +657,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
private void printMention(
PlainTextWriter writer, SignalServiceDataMessage.Mention mention
) {
- final var address = m.resolveSignalServiceAddress(mention.getUuid());
+ final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid()));
writer.println("- {}: {} (length: {})", formatContact(address), mention.getStart(), mention.getLength());
}
diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java
index 5394022e..516224f5 100644
--- a/src/main/java/org/asamk/signal/commands/BlockCommand.java
+++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java
@@ -37,7 +37,7 @@ public class BlockCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var contacts = ns.getList("recipient");
- for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getUsername())) {
+ for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getSelfNumber())) {
try {
m.setContactBlocked(contact, true);
} catch (NotMasterDeviceException e) {
diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java
index 5d637eee..1d6dd26d 100644
--- a/src/main/java/org/asamk/signal/commands/Commands.java
+++ b/src/main/java/org/asamk/signal/commands/Commands.java
@@ -39,6 +39,7 @@ public class Commands {
addCommand(new UnblockCommand());
addCommand(new UnregisterCommand());
addCommand(new UpdateAccountCommand());
+ addCommand(new UpdateConfigurationCommand());
addCommand(new UpdateContactCommand());
addCommand(new UpdateGroupCommand());
addCommand(new UpdateProfileCommand());
diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
index 4a322b99..02063b87 100644
--- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java
+++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
@@ -54,10 +54,10 @@ public class DaemonCommand implements MultiLocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
- boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
+ boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
DBusConnection.DBusBusType busType;
- if (ns.getBoolean("system")) {
+ if (Boolean.TRUE.equals(ns.getBoolean("system"))) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
@@ -83,10 +83,10 @@ public class DaemonCommand implements MultiLocalCommand {
public void handleCommand(
final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter
) throws CommandException {
- boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
+ boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
DBusConnection.DBusBusType busType;
- if (ns.getBoolean("system")) {
+ if (Boolean.TRUE.equals(ns.getBoolean("system"))) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
@@ -95,7 +95,7 @@ public class DaemonCommand implements MultiLocalCommand {
try (var conn = DBusConnection.getConnection(busType)) {
final var signalControl = new DbusSignalControlImpl(c, m -> {
try {
- final var objectPath = DbusConfig.getObjectPath(m.getUsername());
+ final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber());
return run(conn, objectPath, m, outputWriter, ignoreAttachments);
} catch (DBusException e) {
logger.error("Failed to export object", e);
@@ -120,7 +120,10 @@ public class DaemonCommand implements MultiLocalCommand {
private Thread run(
DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments
) throws DBusException {
- conn.exportObject(new DbusSignalImpl(m, objectPath));
+ final var signal = new DbusSignalImpl(m, conn, objectPath);
+ conn.exportObject(signal);
+ final var initThread = new Thread(signal::initObjects);
+ initThread.start();
logger.info("Exported dbus object: " + objectPath);
@@ -136,6 +139,11 @@ public class DaemonCommand implements MultiLocalCommand {
logger.warn("Receiving messages failed, retrying", e);
}
}
+ try {
+ initThread.join();
+ } catch (InterruptedException ignored) {
+ }
+ signal.close();
});
thread.start();
diff --git a/src/main/java/org/asamk/signal/commands/DbusCommand.java b/src/main/java/org/asamk/signal/commands/DbusCommand.java
deleted file mode 100644
index 9f676a39..00000000
--- a/src/main/java/org/asamk/signal/commands/DbusCommand.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.asamk.signal.commands;
-
-import net.sourceforge.argparse4j.inf.Namespace;
-
-import org.asamk.Signal;
-import org.asamk.signal.OutputWriter;
-import org.asamk.signal.commands.exceptions.CommandException;
-import org.asamk.signal.dbus.DbusSignalImpl;
-import org.asamk.signal.manager.Manager;
-
-public interface DbusCommand extends LocalCommand {
-
- void handleCommand(Namespace ns, Signal signal, OutputWriter outputWriter) throws CommandException;
-
- default void handleCommand(
- final Namespace ns, final Manager m, final OutputWriter outputWriter
- ) throws CommandException {
- handleCommand(ns, new DbusSignalImpl(m, null), outputWriter);
- }
-}
diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java
index f5585881..1e06ea9c 100644
--- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java
+++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java
@@ -57,14 +57,14 @@ public class JoinGroupCommand implements JsonRpcLocalCommand {
var newGroupId = results.first();
if (outputWriter instanceof JsonWriter) {
final var writer = (JsonWriter) outputWriter;
- if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) {
+ if (!m.getGroup(newGroupId).isMember()) {
writer.write(Map.of("groupId", newGroupId.toBase64(), "onlyRequested", true));
} else {
writer.write(Map.of("groupId", newGroupId.toBase64()));
}
} else {
final var writer = (PlainTextWriter) outputWriter;
- if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) {
+ if (!m.getGroup(newGroupId).isMember()) {
writer.println("Requested to join group \"{}\"", newGroupId.toBase64());
} else {
writer.println("Joined group \"{}\"", newGroupId.toBase64());
diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java
index d0e4dfec..9af67322 100644
--- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java
+++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java
@@ -65,7 +65,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
- final boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
+ final boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
final var objectMapper = Util.createJsonObjectMapper();
final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter);
diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java
index 24b45ee8..5b926732 100644
--- a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java
+++ b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java
@@ -64,14 +64,5 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand