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..cba438f8 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,47 @@ 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 setProfile(
+ String givenName, String familyName, String about, String aboutEmoji, Optional avatar
+ ) throws IOException;
- /**
- * @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 unregister() 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 deleteAccount() throws IOException;
- account.setRegistered(false);
- }
+ void submitRateLimitRecaptchaChallenge(String challenge, String captcha) 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);
+ List getLinkedDevices() throws IOException;
- dependencies.getAccountManager().deleteAccount();
+ void removeLinkedDevices(int deviceId) throws IOException;
- account.setRegistered(false);
- }
+ void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException;
- public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
- dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
- }
+ void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException;
- 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());
- }
+ Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException;
- public void removeLinkedDevices(int deviceId) throws IOException {
- dependencies.getAccountManager().removeDevice(deviceId);
- var devices = dependencies.getAccountManager().getDevices();
- account.setMultiDevice(devices.size() > 1);
- }
+ List getGroups();
- 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);
- }
-
- 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 +145,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..de60fa50
--- /dev/null
+++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
@@ -0,0 +1,1293 @@
+/*
+ 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);
+ 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());
+ }
+
+ /**
+ * @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(int 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..ff94c19b 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,9 +177,9 @@ 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();
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/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/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
index 0917a214..64e16857 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
@@ -144,7 +144,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)) {
@@ -415,7 +416,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/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/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc
index 6b5d1a86..e7cd083f 100755
--- a/man/signal-cli-dbus.5.adoc
+++ b/man/signal-cli-dbus.5.adoc
@@ -110,12 +110,15 @@ 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)
-* remove : Set to 1 if the existing avatar picture should be removed
+* remove : Set to true if the existing avatar picture should be removed
Exceptions: Failure
diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java
index d411854b..2d53657e 100644
--- a/src/main/java/org/asamk/Signal.java
+++ b/src/main/java/org/asamk/Signal.java
@@ -13,6 +13,8 @@ import java.util.List;
*/
public interface Signal extends DBusInterface {
+ String getSelfNumber();
+
long sendMessage(
String message, List attachments, String recipient
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity;
@@ -26,7 +28,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(
@@ -101,6 +103,15 @@ public interface Signal extends DBusInterface {
void updateDeviceName(String deviceName) throws Error.Failure;
+ 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;
diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java
index 4aa510d6..bffbded5 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;
@@ -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), 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/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/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
index 4a322b99..9878de15 100644
--- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java
+++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
@@ -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);
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/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java
index fbc03300..1d697299 100644
--- a/src/main/java/org/asamk/signal/commands/LinkCommand.java
+++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java
@@ -44,7 +44,7 @@ public class LinkCommand implements ProvisioningCommand {
try {
writer.println("{}", m.getDeviceLinkUri());
try (var manager = m.finishDeviceLink(deviceName)) {
- writer.println("Associated with: {}", manager.getUsername());
+ writer.println("Associated with: {}", manager.getSelfNumber());
}
} catch (TimeoutException e) {
throw new UserErrorException("Link request timed out, please try again.");
@@ -52,7 +52,7 @@ public class LinkCommand implements ProvisioningCommand {
throw new IOErrorException("Link request error: " + e.getMessage(), e);
} catch (UserAlreadyExists e) {
throw new UserErrorException("The user "
- + e.getUsername()
+ + e.getNumber()
+ " already exists\nDelete \""
+ e.getFileName()
+ "\" before trying again.");
diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
index 5e609a48..b6dfc3ce 100644
--- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
@@ -8,10 +8,9 @@ import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.manager.Manager;
+import java.util.UUID;
import java.util.stream.Collectors;
-import static org.asamk.signal.util.Util.getLegacyIdentifier;
-
public class ListContactsCommand implements JsonRpcLocalCommand {
@Override
@@ -33,7 +32,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
for (var c : contacts) {
final var contact = c.second();
writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}",
- getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())),
+ c.first().getLegacyIdentifier(),
contact.getName(),
contact.isBlocked(),
contact.getMessageExpirationTime() == 0
@@ -43,10 +42,10 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
} else {
final var writer = (JsonWriter) outputWriter;
final var jsonContacts = contacts.stream().map(contactPair -> {
- final var address = m.resolveSignalServiceAddress(contactPair.first());
+ final var address = contactPair.first();
final var contact = contactPair.second();
- return new JsonContact(address.getNumber().orNull(),
- address.getUuid().toString(),
+ return new JsonContact(address.getNumber().orElse(null),
+ address.getUuid().map(UUID::toString).orElse(null),
contact.getName(),
contact.isBlocked(),
contact.getMessageExpirationTime());
diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java
index ad0d3531..1de5b842 100644
--- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java
@@ -46,7 +46,7 @@ public class ListDevicesCommand implements JsonRpcLocalCommand {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;
for (var d : devices) {
- writer.println("- Device {}{}:", d.getId(), (d.getId() == m.getDeviceId() ? " (this device)" : ""));
+ writer.println("- Device {}{}:", d.getId(), (d.isThisDevice() ? " (this device)" : ""));
writer.indent(w -> {
w.println("Name: {}", d.getName());
w.println("Created: {}", DateUtils.formatTimestamp(d.getCreated()));
diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
index b53577be..1eda53ce 100644
--- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
@@ -9,13 +9,13 @@ import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
-import org.asamk.signal.manager.storage.groups.GroupInfo;
-import org.asamk.signal.manager.storage.recipients.RecipientId;
-import org.asamk.signal.util.Util;
+import org.asamk.signal.manager.api.Group;
+import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
+import java.util.UUID;
import java.util.stream.Collectors;
public class ListGroupsCommand implements JsonRpcLocalCommand {
@@ -35,44 +35,41 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
.help("List the members and group invite links of each group. If output=json, then this is always set");
}
- private static Set resolveMembers(Manager m, Set addresses) {
- return addresses.stream()
- .map(m::resolveSignalServiceAddress)
- .map(Util::getLegacyIdentifier)
- .collect(Collectors.toSet());
+ private static Set resolveMembers(Set addresses) {
+ return addresses.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toSet());
}
- private static Set resolveJsonMembers(Manager m, Set addresses) {
+ private static Set resolveJsonMembers(Set addresses) {
return addresses.stream()
- .map(m::resolveSignalServiceAddress)
- .map(address -> new JsonGroupMember(address.getNumber().orNull(), address.getUuid().toString()))
+ .map(address -> new JsonGroupMember(address.getNumber().orElse(null),
+ address.getUuid().map(UUID::toString).orElse(null)))
.collect(Collectors.toSet());
}
private static void printGroupPlainText(
- PlainTextWriter writer, Manager m, GroupInfo group, boolean detailed
+ PlainTextWriter writer, Group group, boolean detailed
) {
if (detailed) {
- final var groupInviteLink = group.getGroupInviteLink();
+ final var groupInviteLink = group.getGroupInviteLinkUrl();
writer.println(
"Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Message expiration: {} Link: {}",
group.getGroupId().toBase64(),
group.getTitle(),
group.getDescription(),
- group.isMember(m.getSelfRecipientId()),
+ group.isMember(),
group.isBlocked(),
- resolveMembers(m, group.getMembers()),
- resolveMembers(m, group.getPendingMembers()),
- resolveMembers(m, group.getRequestingMembers()),
- resolveMembers(m, group.getAdminMembers()),
+ resolveMembers(group.getMembers()),
+ resolveMembers(group.getPendingMembers()),
+ resolveMembers(group.getRequestingMembers()),
+ resolveMembers(group.getAdminMembers()),
group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s",
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
} else {
writer.println("Id: {} Name: {} Active: {} Blocked: {}",
group.getGroupId().toBase64(),
group.getTitle(),
- group.isMember(m.getSelfRecipientId()),
+ group.isMember(),
group.isBlocked());
}
}
@@ -87,18 +84,18 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
final var jsonWriter = (JsonWriter) outputWriter;
var jsonGroups = groups.stream().map(group -> {
- final var groupInviteLink = group.getGroupInviteLink();
+ final var groupInviteLink = group.getGroupInviteLinkUrl();
return new JsonGroup(group.getGroupId().toBase64(),
group.getTitle(),
group.getDescription(),
- group.isMember(m.getSelfRecipientId()),
+ group.isMember(),
group.isBlocked(),
group.getMessageExpirationTime(),
- resolveJsonMembers(m, group.getMembers()),
- resolveJsonMembers(m, group.getPendingMembers()),
- resolveJsonMembers(m, group.getRequestingMembers()),
- resolveJsonMembers(m, group.getAdminMembers()),
+ resolveJsonMembers(group.getMembers()),
+ resolveJsonMembers(group.getPendingMembers()),
+ resolveJsonMembers(group.getRequestingMembers()),
+ resolveJsonMembers(group.getAdminMembers()),
groupInviteLink == null ? null : groupInviteLink.getUrl());
}).collect(Collectors.toList());
@@ -107,7 +104,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
final var writer = (PlainTextWriter) outputWriter;
boolean detailed = ns.getBoolean("detailed");
for (var group : groups) {
- printGroupPlainText(writer, m, group, detailed);
+ printGroupPlainText(writer, group, detailed);
}
}
}
diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
index 02cd1d9f..ed2942a5 100644
--- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
@@ -8,7 +8,7 @@ import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
-import org.asamk.signal.manager.storage.identities.IdentityInfo;
+import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util;
@@ -29,9 +29,9 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
return "listIdentities";
}
- private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) {
- final SignalServiceAddress address = m.resolveSignalServiceAddress(theirId.getRecipientId());
- var digits = Util.formatSafetyNumber(m.computeSafetyNumber(address, theirId.getIdentityKey()));
+ private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, Identity theirId) {
+ final SignalServiceAddress address = theirId.getRecipient().toSignalServiceAddress();
+ var digits = Util.formatSafetyNumber(theirId.getSafetyNumber());
writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}",
address.getNumber().orNull(),
theirId.getTrustLevel(),
@@ -52,11 +52,11 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
) throws CommandException {
var number = ns.getString("number");
- List identities;
+ List identities;
if (number == null) {
identities = m.getIdentities();
} else {
- identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getUsername()));
+ identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getSelfNumber()));
}
if (outputWriter instanceof PlainTextWriter) {
@@ -67,9 +67,9 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
} else {
final var writer = (JsonWriter) outputWriter;
final var jsonIdentities = identities.stream().map(id -> {
- final var address = m.resolveSignalServiceAddress(id.getRecipientId());
- var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(address, id.getIdentityKey()));
- var scannableSafetyNumber = m.computeSafetyNumberForScanning(address, id.getIdentityKey());
+ final var address = id.getRecipient().toSignalServiceAddress();
+ var safetyNumber = Util.formatSafetyNumber(id.getSafetyNumber());
+ var scannableSafetyNumber = id.getScannableSafetyNumber();
return new JsonIdentity(address.getNumber().orNull(),
address.getUuid().toString(),
Hex.toString(id.getFingerprint()),
diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
index 67a6596b..7635f8ae 100644
--- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
+++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
@@ -50,7 +50,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand {
) throws CommandException {
final var groupId = CommandUtil.getGroupId(ns.getString("group-id"));
- var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getUsername());
+ var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getSelfNumber());
try {
try {
diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java
index 7d7067c4..e515defe 100644
--- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java
+++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java
@@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
-import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
@@ -17,13 +16,11 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.ErrorUtils;
-import org.freedesktop.dbus.errors.UnknownObject;
-import org.freedesktop.dbus.exceptions.DBusExecutionException;
import java.io.IOException;
import java.util.Map;
-public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand {
+public class RemoteDeleteCommand implements JsonRpcLocalCommand {
@Override
public String getName() {
@@ -69,47 +66,6 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand {
}
}
- @Override
- public void handleCommand(
- final Namespace ns, final Signal signal, final OutputWriter outputWriter
- ) throws CommandException {
- final var recipients = ns.getList("recipient");
- final var groupIdStrings = ns.getList("group-id");
-
- final var noRecipients = recipients == null || recipients.isEmpty();
- final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty();
- if (noRecipients && noGroups) {
- throw new UserErrorException("No recipients given");
- }
- if (!noRecipients && !noGroups) {
- throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
- }
-
- final long targetTimestamp = ns.getLong("target-timestamp");
-
- try {
- long timestamp = 0;
- if (!noGroups) {
- final var groupIds = CommandUtil.getGroupIds(groupIdStrings);
- for (final var groupId : groupIds) {
- timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId.serialize());
- }
- } else {
- timestamp = signal.sendRemoteDeleteMessage(targetTimestamp, recipients);
- }
- outputResult(outputWriter, timestamp);
- } catch (UnknownObject e) {
- throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
- } catch (Signal.Error.InvalidNumber e) {
- throw new UserErrorException("Invalid number: " + e.getMessage());
- } catch (Signal.Error.GroupNotFound e) {
- throw new UserErrorException("Failed to send to group: " + e.getMessage());
- } catch (DBusExecutionException e) {
- throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
- .getSimpleName() + ")", e);
- }
- }
-
private void outputResult(final OutputWriter outputWriter, final long timestamp) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;
diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java
index 1973b1a1..1cd2e674 100644
--- a/src/main/java/org/asamk/signal/commands/SendCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendCommand.java
@@ -4,13 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
-import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
-import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.Manager;
@@ -22,8 +20,6 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.ErrorUtils;
import org.asamk.signal.util.IOUtils;
-import org.freedesktop.dbus.errors.UnknownObject;
-import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,7 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
-public class SendCommand implements DbusCommand, JsonRpcLocalCommand {
+public class SendCommand implements JsonRpcLocalCommand {
private final static Logger logger = LoggerFactory.getLogger(SendCommand.class);
@@ -116,97 +112,6 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand {
}
}
- @Override
- public void handleCommand(
- final Namespace ns, final Signal signal, final OutputWriter outputWriter
- ) throws CommandException {
- final var recipients = ns.getList("recipient");
- final var isEndSession = ns.getBoolean("end-session");
- final var groupIdStrings = ns.getList("group-id");
- final var isNoteToSelf = ns.getBoolean("note-to-self");
-
- final var noRecipients = recipients == null || recipients.isEmpty();
- final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty();
- if ((noRecipients && isEndSession) || (noRecipients && noGroups && !isNoteToSelf)) {
- throw new UserErrorException("No recipients given");
- }
- if (!noRecipients && !noGroups) {
- throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
- }
- if (!noRecipients && isNoteToSelf) {
- throw new UserErrorException(
- "You cannot specify recipients by phone number and note to self at the same time");
- }
-
- if (isEndSession) {
- try {
- signal.sendEndSessionMessage(recipients);
- return;
- } catch (Signal.Error.UntrustedIdentity e) {
- throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
- .getSimpleName() + ")");
- } catch (DBusExecutionException e) {
- throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
- .getSimpleName() + ")", e);
- }
- }
-
- var messageText = ns.getString("message");
- if (messageText == null) {
- try {
- messageText = IOUtils.readAll(System.in, Charset.defaultCharset());
- } catch (IOException e) {
- throw new UserErrorException("Failed to read message from stdin: " + e.getMessage());
- }
- }
-
- List attachments = ns.getList("attachment");
- if (attachments == null) {
- attachments = List.of();
- }
-
- if (!noGroups) {
- final var groupIds = CommandUtil.getGroupIds(groupIdStrings);
-
- try {
- long timestamp = 0;
- for (final var groupId : groupIds) {
- timestamp = signal.sendGroupMessage(messageText, attachments, groupId.serialize());
- }
- outputResult(outputWriter, timestamp);
- return;
- } catch (DBusExecutionException e) {
- throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage(), e);
- }
- }
-
- if (isNoteToSelf) {
- try {
- var timestamp = signal.sendNoteToSelfMessage(messageText, attachments);
- outputResult(outputWriter, timestamp);
- return;
- } catch (Signal.Error.UntrustedIdentity e) {
- throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
- .getSimpleName() + ")");
- } catch (DBusExecutionException e) {
- throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage(), e);
- }
- }
-
- try {
- var timestamp = signal.sendMessage(messageText, attachments, recipients);
- outputResult(outputWriter, timestamp);
- } catch (UnknownObject e) {
- throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
- } catch (Signal.Error.UntrustedIdentity e) {
- throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
- .getSimpleName() + ")");
- } catch (DBusExecutionException e) {
- throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
- .getSimpleName() + ")", e);
- }
- }
-
private void outputResult(final OutputWriter outputWriter, final long timestamp) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;
diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
index 338e70ac..a1c6c319 100644
--- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
@@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
-import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
@@ -17,13 +16,11 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.ErrorUtils;
-import org.freedesktop.dbus.errors.UnknownObject;
-import org.freedesktop.dbus.exceptions.DBusExecutionException;
import java.io.IOException;
import java.util.Map;
-public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand {
+public class SendReactionCommand implements JsonRpcLocalCommand {
@Override
public String getName() {
@@ -72,7 +69,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand {
try {
final var results = m.sendMessageReaction(emoji,
isRemove,
- CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
+ CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
targetTimestamp,
recipientIdentifiers);
outputResult(outputWriter, results.getTimestamp());
@@ -85,54 +82,6 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand {
}
}
- @Override
- public void handleCommand(
- final Namespace ns, final Signal signal, final OutputWriter outputWriter
- ) throws CommandException {
- final var recipients = ns.getList("recipient");
- final var groupIdStrings = ns.getList("group-id");
-
- final var noRecipients = recipients == null || recipients.isEmpty();
- final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty();
- if (noRecipients && noGroups) {
- throw new UserErrorException("No recipients given");
- }
- if (!noRecipients && !noGroups) {
- throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
- }
-
- final var emoji = ns.getString("emoji");
- final var isRemove = ns.getBoolean("remove");
- final var targetAuthor = ns.getString("target-author");
- final var targetTimestamp = ns.getLong("target-timestamp");
-
- try {
- long timestamp = 0;
- if (!noGroups) {
- final var groupIds = CommandUtil.getGroupIds(groupIdStrings);
- for (final var groupId : groupIds) {
- timestamp = signal.sendGroupMessageReaction(emoji,
- isRemove,
- targetAuthor,
- targetTimestamp,
- groupId.serialize());
- }
- } else {
- timestamp = signal.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, recipients);
- }
- outputResult(outputWriter, timestamp);
- } catch (UnknownObject e) {
- throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
- } catch (Signal.Error.InvalidNumber e) {
- throw new UserErrorException("Invalid number: " + e.getMessage());
- } catch (Signal.Error.GroupNotFound e) {
- throw new UserErrorException("Failed to send to group: " + e.getMessage());
- } catch (DBusExecutionException e) {
- throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
- .getSimpleName() + ")", e);
- }
- }
-
private void outputResult(final OutputWriter outputWriter, final long timestamp) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;
diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java
index 0d5772ec..5dd29682 100644
--- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java
@@ -37,7 +37,7 @@ public class SendReceiptCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var recipientString = ns.getString("recipient");
- final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername());
+ final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber());
final var targetTimestamps = ns.getList("target-timestamp");
final var type = ns.getString("type");
diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java
index 3a965e47..cfe66770 100644
--- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java
@@ -45,7 +45,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand {
final var recipientIdentifiers = new HashSet();
if (recipientStrings != null) {
- final var localNumber = m.getUsername();
+ final var localNumber = m.getSelfNumber();
recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber));
}
if (groupIdStrings != null) {
diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java
index aedc2c3e..9e59ad86 100644
--- a/src/main/java/org/asamk/signal/commands/TrustCommand.java
+++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java
@@ -38,7 +38,7 @@ public class TrustCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
var recipentString = ns.getString("recipient");
- var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getUsername());
+ var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getSelfNumber());
if (ns.getBoolean("trust-all-known-keys")) {
boolean res = m.trustIdentityAllKeys(recipient);
if (!res) {
diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java
index 812065bc..7cf209fa 100644
--- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java
@@ -36,7 +36,8 @@ public class UnblockCommand implements JsonRpcLocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
- for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"), m.getUsername())) {
+ for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"),
+ m.getSelfNumber())) {
try {
m.setContactBlocked(contactNumber, false);
} catch (NotMasterDeviceException e) {
diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
index 6c2916eb..46641668 100644
--- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
@@ -33,7 +33,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
var recipientString = ns.getString("recipient");
- var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername());
+ var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber());
try {
var expiration = ns.getInt("expiration");
diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
index b0269894..4bbaa992 100644
--- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
@@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
-import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
@@ -21,17 +20,14 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.ErrorUtils;
-import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.List;
-public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand {
+public class UpdateGroupCommand implements JsonRpcLocalCommand {
private final static Logger logger = LoggerFactory.getLogger(UpdateGroupCommand.class);
@@ -116,7 +112,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand {
final var groupIdString = ns.getString("group-id");
var groupId = CommandUtil.getGroupId(groupIdString);
- final var localNumber = m.getUsername();
+ final var localNumber = m.getSelfNumber();
var groupName = ns.getString("name");
var groupDescription = ns.getString("description");
@@ -179,43 +175,6 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand {
}
}
- @Override
- public void handleCommand(
- final Namespace ns, final Signal signal, final OutputWriter outputWriter
- ) throws CommandException {
- var groupId = CommandUtil.getGroupId(ns.getString("group-id"));
-
- var groupName = ns.getString("name");
- if (groupName == null) {
- groupName = "";
- }
-
- List groupMembers = ns.getList("member");
- if (groupMembers == null) {
- groupMembers = new ArrayList<>();
- }
-
- var groupAvatar = ns.getString("avatar");
- if (groupAvatar == null) {
- groupAvatar = "";
- }
-
- try {
- var newGroupId = signal.updateGroup(groupId == null ? new byte[0] : groupId.serialize(),
- groupName,
- groupMembers,
- groupAvatar);
- if (groupId == null) {
- outputResult(outputWriter, null, GroupId.unknownVersion(newGroupId));
- }
- } catch (Signal.Error.AttachmentInvalid e) {
- throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage());
- } catch (DBusExecutionException e) {
- throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
- .getSimpleName() + ")", e);
- }
- }
-
private void outputResult(final OutputWriter outputWriter, final Long timestamp, final GroupId groupId) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;
diff --git a/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java b/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java
index 84e957cc..819ce495 100644
--- a/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java
+++ b/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java
@@ -5,4 +5,8 @@ public final class UserErrorException extends CommandException {
public UserErrorException(final String message) {
super(message);
}
+
+ public UserErrorException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
new file mode 100644
index 00000000..b9f5ae11
--- /dev/null
+++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
@@ -0,0 +1,487 @@
+package org.asamk.signal.dbus;
+
+import org.asamk.Signal;
+import org.asamk.signal.manager.AttachmentInvalidException;
+import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.NotMasterDeviceException;
+import org.asamk.signal.manager.StickerPackInvalidException;
+import org.asamk.signal.manager.UntrustedIdentityException;
+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.groups.GroupId;
+import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupLinkState;
+import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.GroupPermission;
+import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
+import org.asamk.signal.manager.groups.LastGroupAdminException;
+import org.asamk.signal.manager.groups.NotAGroupMemberException;
+import org.asamk.signal.manager.storage.recipients.Contact;
+import org.asamk.signal.manager.storage.recipients.Profile;
+import org.asamk.signal.manager.storage.recipients.RecipientAddress;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.libsignal.util.Pair;
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
+import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * This class implements the Manager interface using the DBus Signal interface, where possible.
+ * It's used for the signal-cli dbus client mode (--dbus, --dbus-system)
+ */
+public class DbusManagerImpl implements Manager {
+
+ private final Signal signal;
+
+ public DbusManagerImpl(final Signal signal) {
+ this.signal = signal;
+ }
+
+ @Override
+ public String getSelfNumber() {
+ return signal.getSelfNumber();
+ }
+
+ @Override
+ public void checkAccountState() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Map> areUsersRegistered(final Set numbers) throws IOException {
+ final var numbersList = new ArrayList<>(numbers);
+ final var registered = signal.isRegistered(numbersList);
+
+ final var result = new HashMap>();
+ for (var i = 0; i < numbersList.size(); i++) {
+ result.put(numbersList.get(i),
+ new Pair<>(numbersList.get(i), registered.get(i) ? UuidUtil.UNKNOWN_UUID : null));
+ }
+ return result;
+ }
+
+ @Override
+ public void updateAccountAttributes(final String deviceName) throws IOException {
+ if (deviceName != null) {
+ signal.updateDeviceName(deviceName);
+ }
+ }
+
+ @Override
+ public void setProfile(
+ final String givenName,
+ final String familyName,
+ final String about,
+ final String aboutEmoji,
+ final Optional avatar
+ ) throws IOException {
+ signal.updateProfile(emptyIfNull(givenName),
+ emptyIfNull(familyName),
+ emptyIfNull(about),
+ emptyIfNull(aboutEmoji),
+ avatar == null ? "" : avatar.transform(File::getPath).or(""),
+ avatar != null && !avatar.isPresent());
+ }
+
+ @Override
+ public void unregister() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void deleteAccount() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List getLinkedDevices() throws IOException {
+ return signal.listDevices()
+ .stream()
+ .map(name -> new Device(-1, name, 0, 0, false))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void removeLinkedDevices(final int deviceId) throws IOException {
+ signal.removeDevice(deviceId);
+ }
+
+ @Override
+ public void addDeviceLink(final URI linkUri) throws IOException, InvalidKeyException {
+ signal.addDevice(linkUri.toString());
+ }
+
+ @Override
+ public void setRegistrationLockPin(final Optional pin) throws IOException, UnauthenticatedResponseException {
+ if (pin.isPresent()) {
+ signal.setPin(pin.get());
+ } else {
+ signal.removePin();
+ }
+ }
+
+ @Override
+ public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List getGroups() {
+ final var groupIds = signal.getGroupIds();
+ return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList());
+ }
+
+ @Override
+ public SendGroupMessageResults quitGroup(
+ final GroupId groupId, final Set groupAdmins
+ ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
+ if (groupAdmins.size() > 0) {
+ throw new UnsupportedOperationException();
+ }
+ signal.quitGroup(groupId.serialize());
+ return new SendGroupMessageResults(0, List.of());
+ }
+
+ @Override
+ public void deleteGroup(final GroupId groupId) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Pair createGroup(
+ final String name, final Set members, final File avatarFile
+ ) throws IOException, AttachmentInvalidException {
+ final var newGroupId = signal.updateGroup(new byte[0],
+ emptyIfNull(name),
+ members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
+ avatarFile == null ? "" : avatarFile.getPath());
+ return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
+ }
+
+ @Override
+ public SendGroupMessageResults updateGroup(
+ final GroupId groupId,
+ final String name,
+ final String description,
+ final Set members,
+ final Set removeMembers,
+ final Set admins,
+ final Set removeAdmins,
+ final boolean resetGroupLink,
+ final GroupLinkState groupLinkState,
+ final GroupPermission addMemberPermission,
+ final GroupPermission editDetailsPermission,
+ final File avatarFile,
+ final Integer expirationTimer,
+ final Boolean isAnnouncementGroup
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
+ signal.updateGroup(groupId.serialize(),
+ emptyIfNull(name),
+ members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
+ avatarFile == null ? "" : avatarFile.getPath());
+ return new SendGroupMessageResults(0, List.of());
+ }
+
+ @Override
+ public Pair joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException {
+ final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
+ return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
+ }
+
+ @Override
+ public void sendTypingMessage(
+ final TypingAction action, final Set recipients
+ ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ for (final var recipient : recipients) {
+ if (recipient instanceof RecipientIdentifier.Single) {
+ signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(),
+ action == TypingAction.STOP);
+ } else if (recipient instanceof RecipientIdentifier.Group) {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Override
+ public void sendReadReceipt(
+ final RecipientIdentifier.Single sender, final List messageIds
+ ) throws IOException, UntrustedIdentityException {
+ signal.sendReadReceipt(sender.getIdentifier(), messageIds);
+ }
+
+ @Override
+ public void sendViewedReceipt(
+ final RecipientIdentifier.Single sender, final List messageIds
+ ) throws IOException, UntrustedIdentityException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public SendMessageResults sendMessage(
+ final Message message, final Set recipients
+ ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ return handleMessage(recipients,
+ numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers),
+ () -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()),
+ groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId));
+ }
+
+ @Override
+ public SendMessageResults sendRemoteDeleteMessage(
+ final long targetSentTimestamp, final Set recipients
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ return handleMessage(recipients,
+ numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
+ () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
+ groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
+ }
+
+ @Override
+ public SendMessageResults sendMessageReaction(
+ final String emoji,
+ final boolean remove,
+ final RecipientIdentifier.Single targetAuthor,
+ final long targetSentTimestamp,
+ final Set recipients
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ return handleMessage(recipients,
+ numbers -> signal.sendMessageReaction(emoji,
+ remove,
+ targetAuthor.getIdentifier(),
+ targetSentTimestamp,
+ numbers),
+ () -> signal.sendMessageReaction(emoji,
+ remove,
+ targetAuthor.getIdentifier(),
+ targetSentTimestamp,
+ signal.getSelfNumber()),
+ groupId -> signal.sendGroupMessageReaction(emoji,
+ remove,
+ targetAuthor.getIdentifier(),
+ targetSentTimestamp,
+ groupId));
+ }
+
+ @Override
+ public SendMessageResults sendEndSessionMessage(final Set recipients) throws IOException {
+ signal.sendEndSessionMessage(recipients.stream()
+ .map(RecipientIdentifier.Single::getIdentifier)
+ .collect(Collectors.toList()));
+ return new SendMessageResults(0, Map.of());
+ }
+
+ @Override
+ public void setContactName(
+ final RecipientIdentifier.Single recipient, final String name
+ ) throws NotMasterDeviceException, UnregisteredUserException {
+ signal.setContactName(recipient.getIdentifier(), name);
+ }
+
+ @Override
+ public void setContactBlocked(
+ final RecipientIdentifier.Single recipient, final boolean blocked
+ ) throws NotMasterDeviceException, IOException {
+ signal.setContactBlocked(recipient.getIdentifier(), blocked);
+ }
+
+ @Override
+ public void setGroupBlocked(
+ final GroupId groupId, final boolean blocked
+ ) throws GroupNotFoundException, IOException {
+ signal.setGroupBlocked(groupId.serialize(), blocked);
+ }
+
+ @Override
+ public void setExpirationTimer(
+ final RecipientIdentifier.Single recipient, final int messageExpirationTimer
+ ) throws IOException {
+ signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
+ }
+
+ @Override
+ public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
+ try {
+ return new URI(signal.uploadStickerPack(path.getPath()));
+ } catch (URISyntaxException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ public void requestAllSyncData() throws IOException {
+ signal.sendSyncRequest();
+ }
+
+ @Override
+ public void receiveMessages(
+ final long timeout,
+ final TimeUnit unit,
+ final boolean returnOnTimeout,
+ final boolean ignoreAttachments,
+ final ReceiveMessageHandler handler
+ ) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasCaughtUpWithOldMessages() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
+ return signal.isContactBlocked(recipient.getIdentifier());
+ }
+
+ @Override
+ public File getAttachmentFile(final SignalServiceAttachmentRemoteId attachmentId) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void sendContacts() throws IOException {
+ signal.sendContacts();
+ }
+
+ @Override
+ public List> getContacts() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
+ return signal.getContactName(recipient.getIdentifier());
+ }
+
+ @Override
+ public Group getGroup(final GroupId groupId) {
+ final var id = groupId.serialize();
+ return new Group(groupId,
+ signal.getGroupName(id),
+ null,
+ null,
+ signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()),
+ Set.of(),
+ Set.of(),
+ Set.of(),
+ signal.isGroupBlocked(id),
+ 0,
+ false,
+ signal.isMember(id));
+ }
+
+ @Override
+ public List getIdentities() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List getIdentities(final RecipientIdentifier.Single recipient) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean trustIdentityVerifiedSafetyNumber(
+ final RecipientIdentifier.Single recipient, final String safetyNumber
+ ) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean trustIdentityVerifiedSafetyNumber(
+ final RecipientIdentifier.Single recipient, final byte[] safetyNumber
+ ) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String computeSafetyNumber(
+ final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
+ ) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
+ return address;
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+
+ private SendMessageResults handleMessage(
+ Set recipients,
+ Function, Long> recipientsHandler,
+ Supplier noteToSelfHandler,
+ Function groupHandler
+ ) {
+ long timestamp = 0;
+ final var singleRecipients = recipients.stream()
+ .filter(r -> r instanceof RecipientIdentifier.Single)
+ .map(RecipientIdentifier.Single.class::cast)
+ .map(RecipientIdentifier.Single::getIdentifier)
+ .collect(Collectors.toList());
+ if (singleRecipients.size() > 0) {
+ timestamp = recipientsHandler.apply(singleRecipients);
+ }
+
+ if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
+ timestamp = noteToSelfHandler.get();
+ }
+ final var groupRecipients = recipients.stream()
+ .filter(r -> r instanceof RecipientIdentifier.Group)
+ .map(RecipientIdentifier.Group.class::cast)
+ .map(g -> g.groupId)
+ .collect(Collectors.toList());
+ for (final var groupId : groupRecipients) {
+ timestamp = groupHandler.apply(groupId.serialize());
+ }
+ return new SendMessageResults(timestamp, Map.of());
+ }
+
+ private String emptyIfNull(final String string) {
+ return string == null ? "" : string;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java
index 125a93a7..969d2239 100644
--- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java
+++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java
@@ -165,7 +165,7 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
synchronized (receiveThreads) {
return receiveThreads.stream()
.map(Pair::first)
- .map(Manager::getUsername)
+ .map(Manager::getSelfNumber)
.map(u -> new DBusPath(DbusConfig.getObjectPath(u)))
.collect(Collectors.toList());
}
diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
index d22e502a..babb90b3 100644
--- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
+++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
@@ -8,6 +8,7 @@ import org.asamk.signal.manager.NotMasterDeviceException;
import org.asamk.signal.manager.StickerPackInvalidException;
import org.asamk.signal.manager.UntrustedIdentityException;
import org.asamk.signal.manager.api.Device;
+import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.TypingAction;
@@ -17,9 +18,9 @@ import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
-import org.asamk.signal.manager.storage.identities.IdentityInfo;
+import org.asamk.signal.manager.storage.recipients.Profile;
+import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.util.ErrorUtils;
-import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
@@ -45,8 +46,6 @@ import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static org.asamk.signal.util.Util.getLegacyIdentifier;
-
public class DbusSignalImpl implements Signal {
private final Manager m;
@@ -67,6 +66,11 @@ public class DbusSignalImpl implements Signal {
return objectPath;
}
+ @Override
+ public String getSelfNumber() {
+ return m.getSelfNumber();
+ }
+
@Override
public void addDevice(String uri) {
try {
@@ -92,8 +96,6 @@ public class DbusSignalImpl implements Signal {
@Override
public List listDevices() {
List devices;
- List results = new ArrayList();
-
try {
devices = m.getLinkedDevices();
} catch (IOException | Error.Failure e) {
@@ -123,7 +125,7 @@ public class DbusSignalImpl implements Signal {
public long sendMessage(final String message, final List attachments, final List recipients) {
try {
final var results = m.sendMessage(new Message(message, attachments),
- getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
+ getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet()));
@@ -153,7 +155,7 @@ public class DbusSignalImpl implements Signal {
) {
try {
final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
- getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
+ getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet()));
checkSendMessageResults(results.getTimestamp(), results.getResults());
@@ -205,9 +207,9 @@ public class DbusSignalImpl implements Signal {
try {
final var results = m.sendMessageReaction(emoji,
remove,
- getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
+ getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
targetSentTimestamp,
- getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
+ getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet()));
checkSendMessageResults(results.getTimestamp(), results.getResults());
@@ -227,7 +229,7 @@ public class DbusSignalImpl implements Signal {
var recipients = new ArrayList(1);
recipients.add(recipient);
m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START,
- getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
+ getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet()));
} catch (IOException e) {
@@ -241,10 +243,10 @@ public class DbusSignalImpl implements Signal {
@Override
public void sendReadReceipt(
- final String recipient, final List timestamps
+ final String recipient, final List messageIds
) throws Error.Failure, Error.UntrustedIdentity {
try {
- m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getUsername()), timestamps);
+ m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()), messageIds);
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (UntrustedIdentityException e) {
@@ -276,7 +278,7 @@ public class DbusSignalImpl implements Signal {
) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
try {
final var results = m.sendMessage(new Message(message, attachments),
- Set.of(new RecipientIdentifier.NoteToSelf()));
+ Set.of(RecipientIdentifier.NoteToSelf.INSTANCE));
checkSendMessageResults(results.getTimestamp(), results.getResults());
return results.getTimestamp();
} catch (AttachmentInvalidException e) {
@@ -291,7 +293,7 @@ public class DbusSignalImpl implements Signal {
@Override
public void sendEndSessionMessage(final List recipients) {
try {
- final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername()));
+ final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber()));
checkSendMessageResults(results.getTimestamp(), results.getResults());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
@@ -325,7 +327,7 @@ public class DbusSignalImpl implements Signal {
try {
final var results = m.sendMessageReaction(emoji,
remove,
- getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
+ getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
targetSentTimestamp,
Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
checkSendMessageResults(results.getTimestamp(), results.getResults());
@@ -341,13 +343,14 @@ public class DbusSignalImpl implements Signal {
// the profile name
@Override
public String getContactName(final String number) {
- return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername()));
+ final var name = m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber()));
+ return name == null ? "" : name;
}
@Override
public void setContactName(final String number, final String name) {
try {
- m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name);
+ m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name);
} catch (NotMasterDeviceException e) {
throw new Error.Failure("This command doesn't work on linked devices.");
} catch (UnregisteredUserException e) {
@@ -358,7 +361,7 @@ public class DbusSignalImpl implements Signal {
@Override
public void setExpirationTimer(final String number, final int expiration) {
try {
- m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getUsername()), expiration);
+ m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getSelfNumber()), expiration);
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
@@ -367,7 +370,7 @@ public class DbusSignalImpl implements Signal {
@Override
public void setContactBlocked(final String number, final boolean blocked) {
try {
- m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked);
+ m.setContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()), blocked);
} catch (NotMasterDeviceException e) {
throw new Error.Failure("This command doesn't work on linked devices.");
} catch (IOException e) {
@@ -399,7 +402,7 @@ public class DbusSignalImpl implements Signal {
@Override
public String getGroupName(final byte[] groupId) {
var group = m.getGroup(getGroupId(groupId));
- if (group == null) {
+ if (group == null || group.getTitle() == null) {
return "";
} else {
return group.getTitle();
@@ -412,27 +415,17 @@ public class DbusSignalImpl implements Signal {
if (group == null) {
return List.of();
} else {
- return group.getMembers()
- .stream()
- .map(m::resolveSignalServiceAddress)
- .map(Util::getLegacyIdentifier)
- .collect(Collectors.toList());
+ return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
}
}
@Override
public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) {
try {
- if (groupId.length == 0) {
- groupId = null;
- }
- if (name.isEmpty()) {
- name = null;
- }
- if (avatar.isEmpty()) {
- avatar = null;
- }
- final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername());
+ groupId = nullIfEmpty(groupId);
+ name = nullIfEmpty(name);
+ avatar = nullIfEmpty(avatar);
+ final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber());
if (groupId == null) {
final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar));
checkSendMessageResults(results.second().getTimestamp(), results.second().getResults());
@@ -497,6 +490,30 @@ public class DbusSignalImpl implements Signal {
}).collect(Collectors.toList());
}
+ @Override
+ public void updateProfile(
+ String givenName,
+ String familyName,
+ String about,
+ String aboutEmoji,
+ String avatarPath,
+ final boolean removeAvatar
+ ) {
+ try {
+ givenName = nullIfEmpty(givenName);
+ familyName = nullIfEmpty(familyName);
+ about = nullIfEmpty(about);
+ aboutEmoji = nullIfEmpty(aboutEmoji);
+ avatarPath = nullIfEmpty(avatarPath);
+ Optional avatarFile = removeAvatar
+ ? Optional.absent()
+ : avatarPath == null ? null : Optional.of(new File(avatarPath));
+ m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile);
+ } catch (IOException e) {
+ throw new Error.Failure(e.getMessage());
+ }
+ }
+
@Override
public void updateProfile(
final String name,
@@ -505,17 +522,7 @@ public class DbusSignalImpl implements Signal {
String avatarPath,
final boolean removeAvatar
) {
- try {
- if (avatarPath.isEmpty()) {
- avatarPath = null;
- }
- Optional avatarFile = removeAvatar
- ? Optional.absent()
- : avatarPath == null ? null : Optional.of(new File(avatarPath));
- m.setProfile(name, null, about, aboutEmoji, avatarFile);
- } catch (IOException e) {
- throw new Error.Failure(e.getMessage());
- }
+ updateProfile(name, "", about, aboutEmoji, avatarPath, removeAvatar);
}
@Override
@@ -551,10 +558,9 @@ public class DbusSignalImpl implements Signal {
// all numbers the system knows
@Override
public List listNumbers() {
- return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
+ return Stream.concat(m.getIdentities().stream().map(Identity::getRecipient),
m.getContacts().stream().map(Pair::first))
- .map(m::resolveSignalServiceAddress)
- .map(a -> a.getNumber().orNull())
+ .map(a -> a.getNumber().orElse(null))
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
@@ -567,16 +573,19 @@ public class DbusSignalImpl implements Signal {
var contacts = m.getContacts();
for (var c : contacts) {
if (name.equals(c.second().getName())) {
- numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())));
+ numbers.add(c.first().getLegacyIdentifier());
}
}
// Try profiles if no contact name was found
for (var identity : m.getIdentities()) {
- final var recipientId = identity.getRecipientId();
- final var address = m.resolveSignalServiceAddress(recipientId);
- var number = address.getNumber().orNull();
+ final var address = identity.getRecipient();
+ var number = address.getNumber().orElse(null);
if (number != null) {
- var profile = m.getRecipientProfile(recipientId);
+ Profile profile = null;
+ try {
+ profile = m.getRecipientProfile(RecipientIdentifier.Single.fromAddress(address));
+ } catch (UnregisteredUserException ignored) {
+ }
if (profile != null && profile.getDisplayName().equals(name)) {
numbers.add(number);
}
@@ -617,7 +626,7 @@ public class DbusSignalImpl implements Signal {
@Override
public boolean isContactBlocked(final String number) {
- return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()));
+ return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()));
}
@Override
@@ -636,7 +645,7 @@ public class DbusSignalImpl implements Signal {
if (group == null) {
return false;
} else {
- return group.isMember(m.getSelfRecipientId());
+ return group.isMember();
}
}
@@ -747,4 +756,12 @@ public class DbusSignalImpl implements Signal {
throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage());
}
}
+
+ private byte[] nullIfEmpty(final byte[] array) {
+ return array.length == 0 ? null : array;
+ }
+
+ private String nullIfEmpty(final String name) {
+ return name.isEmpty() ? null : name;
+ }
}
diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java
index b24768b7..3c6f2eec 100644
--- a/src/main/java/org/asamk/signal/json/JsonMention.java
+++ b/src/main/java/org/asamk/signal/json/JsonMention.java
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import static org.asamk.signal.util.Util.getLegacyIdentifier;
@@ -26,7 +27,7 @@ public class JsonMention {
final int length;
JsonMention(SignalServiceDataMessage.Mention mention, Manager m) {
- final var address = m.resolveSignalServiceAddress(mention.getUuid());
+ final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid()));
this.name = getLegacyIdentifier(address);
this.number = address.getNumber().orNull();
this.uuid = address.getUuid().toString();
diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
index 7b884b0e..e49e6125 100644
--- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
+++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
@@ -86,7 +86,7 @@ public class JsonMessageEnvelope {
}
String name;
try {
- name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getUsername()));
+ name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getSelfNumber()));
} catch (InvalidNumberException | NullPointerException e) {
name = null;
}
diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java
index 83674876..0a624e6b 100644
--- a/src/main/java/org/asamk/signal/util/CommandUtil.java
+++ b/src/main/java/org/asamk/signal/util/CommandUtil.java
@@ -25,10 +25,10 @@ public class CommandUtil {
) throws UserErrorException {
final var recipientIdentifiers = new HashSet();
if (isNoteToSelf) {
- recipientIdentifiers.add(new RecipientIdentifier.NoteToSelf());
+ recipientIdentifiers.add(RecipientIdentifier.NoteToSelf.INSTANCE);
}
if (recipientStrings != null) {
- final var localNumber = m.getUsername();
+ final var localNumber = m.getSelfNumber();
recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber));
}
if (groupIdStrings != null) {