From ba817e2ae4147b201fbb3e5eb8c86e359873ec02 Mon Sep 17 00:00:00 2001 From: John Freed Date: Tue, 28 Sep 2021 18:41:10 +0200 Subject: [PATCH 1/8] Implement Dbus updateProfile with givenName (#734) two versions of updateProfile implemented: - one with givenName and familyName - one with just name update documentation --- man/signal-cli-dbus.5.adoc | 9 +++++--- src/main/java/org/asamk/Signal.java | 4 ++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) 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 3bfeb5bd..59aa03ce 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -101,6 +101,10 @@ 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/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 63764a2f..c73918ef 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -497,6 +497,28 @@ public class DbusSignalImpl implements Signal { }).collect(Collectors.toList()); } + @Override + public void updateProfile( + final String givenName, + final String familyName, + final String about, + final String aboutEmoji, + 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(givenName, familyName, about, aboutEmoji, avatarFile); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + @Override public void updateProfile( final String name, From 4acab9043c1f479adf735e193f9404a014b52ed7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 18:42:05 +0200 Subject: [PATCH 2/8] Reformat code --- src/main/java/org/asamk/Signal.java | 7 ++++++- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 59aa03ce..b19fba8d 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -102,7 +102,12 @@ 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 + String givenName, + String familyName, + String about, + String aboutEmoji, + String avatarPath, + boolean removeAvatar ) throws Error.Failure; void updateProfile( diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index c73918ef..12cf7d4c 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -505,14 +505,14 @@ public class DbusSignalImpl implements Signal { final String aboutEmoji, String avatarPath, final boolean removeAvatar - ) { + ) { try { if (avatarPath.isEmpty()) { avatarPath = null; } Optional avatarFile = removeAvatar ? Optional.absent() - : avatarPath == null ? null : Optional.of(new File(avatarPath)); + : avatarPath == null ? null : Optional.of(new File(avatarPath)); m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile); } catch (IOException e) { throw new Error.Failure(e.getMessage()); From 7c9fd9d0fb7b303e8194a6de9aed852c488afc25 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 21:11:53 +0200 Subject: [PATCH 3/8] Refactor NoteToSelf to singleton class --- .../asamk/signal/manager/api/RecipientIdentifier.java | 9 ++------- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 2 +- src/main/java/org/asamk/signal/util/CommandUtil.java | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) 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..cb0a08bb 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 @@ -12,14 +12,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() { } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 12cf7d4c..e975a671 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -276,7 +276,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) { diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java index 83674876..18b38a2a 100644 --- a/src/main/java/org/asamk/signal/util/CommandUtil.java +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -25,7 +25,7 @@ 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(); From 1a81bbecbb1d40ef08ab6b3b1913dfe73c678262 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 21:12:37 +0200 Subject: [PATCH 4/8] Do not send message resend request to own device --- .../asamk/signal/manager/helper/IncomingMessageHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..45173da4 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)) { From b91c162159c7c28d049ceb8889c419791573d3bb Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 15 Sep 2021 21:40:47 +0200 Subject: [PATCH 5/8] Extract Manager interface --- .../org/asamk/signal/manager/Manager.java | 1134 ++------------- .../org/asamk/signal/manager/ManagerImpl.java | 1240 +++++++++++++++++ .../signal/manager/ProvisioningManager.java | 6 +- .../signal/manager/RegistrationManager.java | 4 +- 4 files changed, 1324 insertions(+), 1060 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java 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..d2eb0f8f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,22 +1,5 @@ -/* - 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.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -25,7 +8,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,233 +16,46 @@ 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.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( + static Manager init( String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent, - final TrustNewIdentity trustNewIdentity + TrustNewIdentity trustNewIdentity ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); @@ -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 getAllLocalUsernames(File settingsPath) { var pathConfig = PathConfig.createDefault(settingsPath); final var dataPath = pathConfig.getDataPath(); final var files = dataPath.listFiles(); @@ -295,208 +90,51 @@ 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 getUsername(); - /** - * 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 ""; - } - })); + RecipientId getSelfRecipientId(); - // 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())); + int getDeviceId(); - 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 checkAccountState() 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()); - } + Map> areUsersRegistered(Set numbers) 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 updateAccountAttributes(String deviceName) 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 setProfile( + String givenName, String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException; - account.setRegistered(false); - } + void unregister() throws IOException; - public void deleteAccount() throws IOException { - try { - pinHelper.removeRegistrationLockPin(); - } catch (UnauthenticatedResponseException e) { - logger.warn("Failed to remove registration lock pin"); - } - account.setRegistrationLockPin(null, null); + void deleteAccount() throws IOException; - dependencies.getAccountManager().deleteAccount(); + void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException; - account.setRegistered(false); - } + List getLinkedDevices() throws IOException; - public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { - dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); - } + void removeLinkedDevices(int deviceId) throws IOException; - public List getLinkedDevices() throws IOException { - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); - var identityKey = account.getIdentityKeyPair().getPrivateKey(); - return devices.stream().map(d -> { - String deviceName = d.getName(); - if (deviceName != null) { - try { - deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey); - } catch (IOException e) { - logger.debug("Failed to decrypt device name, maybe plain text?", e); - } - } - return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); - }).collect(Collectors.toList()); - } + void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException; - public void removeLinkedDevices(int deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); - } + void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException; - public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + Profile getRecipientProfile(RecipientId recipientId); - addDevice(info.deviceIdentifier, info.deviceKey); - } + List getGroups(); - 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 +149,110 @@ 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 recipientIdentifier); - final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); - queuedActions.addAll(result.first()); - final var exception = result.second(); + GroupInfo 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; - } + byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - final var contact = account.getContactStore().getContact(recipientId); - if (contact != null && !Util.isEmpty(contact.getName())) { - return contact.getName(); - } + SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); - final var profile = getRecipientProfile(recipientId); - if (profile != null) { - return profile.getDisplayName(); - } + SignalServiceAddress resolveSignalServiceAddress(UUID uuid); - 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(RecipientId recipientId); @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..d0fab350 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -0,0 +1,1240 @@ +/* + 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.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.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::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); + } + + @Override + public String getUsername() { + return account.getUsername(); + } + + @Override + public RecipientId getSelfRecipientId() { + return account.getSelfRecipientId(); + } + + @Override + public int getDeviceId() { + return account.getDeviceId(); + } + + @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()); + }).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(RecipientId recipientId) { + return profileHelper.getRecipientProfile(recipientId); + } + + @Override + public List getGroups() { + return account.getGroupStore().getGroups(); + } + + @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(); + } + + @Override + 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; + } + + @Override + public GroupInfo getGroup(GroupId groupId) { + return groupHelper.getGroup(groupId); + } + + @Override + public List getIdentities() { + return account.getIdentityKeyStore().getIdentities(); + } + + @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(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(); + } + + @Override + 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); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { + return resolveSignalServiceAddress(resolveRecipient(address)); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); + } + + @Override + 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); + } + + @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..978f1fd5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -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(); From d72b838560b1a4186ac121c7d605773b49fcdf46 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 18 Sep 2021 10:19:56 +0200 Subject: [PATCH 6/8] Refactor Manager interface --- .../org/asamk/signal/manager/Manager.java | 40 +++--- .../org/asamk/signal/manager/ManagerImpl.java | 123 +++++++++++++----- .../signal/manager/RegistrationManager.java | 8 +- .../signal/manager/UserAlreadyExists.java | 10 +- .../org/asamk/signal/manager/api/Device.java | 8 +- .../org/asamk/signal/manager/api/Group.java | 99 ++++++++++++++ .../asamk/signal/manager/api/Identity.java | 65 +++++++++ .../manager/api/RecipientIdentifier.java | 22 ++++ .../storage/recipients/RecipientAddress.java | 10 ++ src/main/java/org/asamk/Signal.java | 4 +- src/main/java/org/asamk/signal/App.java | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 8 +- .../asamk/signal/commands/BlockCommand.java | 2 +- .../asamk/signal/commands/DaemonCommand.java | 2 +- .../signal/commands/JoinGroupCommand.java | 4 +- .../asamk/signal/commands/LinkCommand.java | 4 +- .../signal/commands/ListContactsCommand.java | 11 +- .../signal/commands/ListDevicesCommand.java | 2 +- .../signal/commands/ListGroupsCommand.java | 49 ++++--- .../commands/ListIdentitiesCommand.java | 18 +-- .../signal/commands/QuitGroupCommand.java | 2 +- .../signal/commands/SendReactionCommand.java | 2 +- .../signal/commands/SendReceiptCommand.java | 2 +- .../signal/commands/SendTypingCommand.java | 2 +- .../asamk/signal/commands/TrustCommand.java | 2 +- .../asamk/signal/commands/UnblockCommand.java | 3 +- .../signal/commands/UpdateContactCommand.java | 2 +- .../signal/commands/UpdateGroupCommand.java | 2 +- .../signal/dbus/DbusSignalControlImpl.java | 2 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 68 +++++----- .../org/asamk/signal/json/JsonMention.java | 3 +- .../signal/json/JsonMessageEnvelope.java | 2 +- .../org/asamk/signal/util/CommandUtil.java | 2 +- 33 files changed, 416 insertions(+), 169 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/Group.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/Identity.java 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 d2eb0f8f..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,6 +1,8 @@ package org.asamk.signal.manager; 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; @@ -17,12 +19,10 @@ 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.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.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; -import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; @@ -51,7 +51,7 @@ import java.util.stream.Collectors; public interface Manager extends Closeable { static Manager init( - String username, + String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent, @@ -59,11 +59,11 @@ public interface Manager extends Closeable { ) 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(); @@ -74,7 +74,7 @@ public interface Manager extends Closeable { return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); } - 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(); @@ -90,11 +90,7 @@ public interface Manager extends Closeable { .collect(Collectors.toList()); } - String getUsername(); - - RecipientId getSelfRecipientId(); - - int getDeviceId(); + String getSelfNumber(); void checkAccountState() throws IOException; @@ -120,9 +116,9 @@ public interface Manager extends Closeable { void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException; - Profile getRecipientProfile(RecipientId recipientId); + Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException; - List getGroups(); + List getGroups(); SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins @@ -221,15 +217,15 @@ public interface Manager extends Closeable { void sendContacts() throws IOException; - List> getContacts(); + List> getContacts(); - String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier); + String getContactOrProfileName(RecipientIdentifier.Single recipient); - GroupInfo getGroup(GroupId groupId); + Group getGroup(GroupId groupId); - List getIdentities(); + List getIdentities(); - List getIdentities(RecipientIdentifier.Single recipient); + List getIdentities(RecipientIdentifier.Single recipient); boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint); @@ -241,14 +237,8 @@ public interface Manager extends Closeable { String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); - SignalServiceAddress resolveSignalServiceAddress(UUID uuid); - - SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId); - @Override void close() throws IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index d0fab350..de60fa50 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -18,6 +18,8 @@ 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; @@ -52,6 +54,7 @@ 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; @@ -196,7 +199,7 @@ public class ManagerImpl implements Manager { this::resolveSignalServiceAddress, account.getRecipientStore(), this::handleIdentityFailure, - this::getGroup, + this::getGroupInfo, this::refreshRegisteredUser); this.groupHelper = new GroupHelper(account, dependencies, @@ -240,20 +243,10 @@ public class ManagerImpl implements Manager { } @Override - public String getUsername() { + public String getSelfNumber() { return account.getUsername(); } - @Override - public RecipientId getSelfRecipientId() { - return account.getSelfRecipientId(); - } - - @Override - public int getDeviceId() { - return account.getDeviceId(); - } - @Override public void checkAccountState() throws IOException { if (account.getLastReceiveTimestamp() == 0) { @@ -385,7 +378,11 @@ public class ManagerImpl implements Manager { logger.debug("Failed to decrypt device name, maybe plain text?", e); } } - return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); + return new Device(d.getId(), + deviceName, + d.getCreated(), + d.getLastSeen(), + d.getId() == account.getDeviceId()); }).collect(Collectors.toList()); } @@ -442,13 +439,48 @@ public class ManagerImpl implements Manager { } @Override - public Profile getRecipientProfile(RecipientId recipientId) { + 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(); + 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 @@ -973,15 +1005,19 @@ public class ManagerImpl implements Manager { } @Override - public List> getContacts() { - return account.getContactStore().getContacts(); + 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 recipientIdentifier) { + public String getContactOrProfileName(RecipientIdentifier.Single recipient) { final RecipientId recipientId; try { - recipientId = resolveRecipient(recipientIdentifier); + recipientId = resolveRecipient(recipient); } catch (UnregisteredUserException e) { return null; } @@ -1000,24 +1036,46 @@ public class ManagerImpl implements Manager { } @Override - public GroupInfo getGroup(GroupId groupId) { + 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(); + 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) { + 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); + return identity == null ? List.of() : List.of(toIdentity(identity)); } /** @@ -1144,8 +1202,7 @@ public class ManagerImpl implements Manager { return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); } - @Override - public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + private byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); } @@ -1165,13 +1222,7 @@ public class ManagerImpl implements Manager { return resolveSignalServiceAddress(resolveRecipient(address)); } - @Override - public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); - } - - @Override - public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + private SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); if (address.getUuid().isPresent()) { return address.toSignalServiceAddress(); @@ -1180,13 +1231,15 @@ public class ManagerImpl implements Manager { // 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 { - return resolveSignalServiceAddress(getRegisteredUser(number)); + 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 { 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 978f1fd5..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); } 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 cb0a08bb..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; @@ -29,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 { @@ -53,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 { @@ -77,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/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/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index b19fba8d..2105ca76 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 getNumber(); + 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( diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 4aa510d6..e81b7018 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -161,7 +161,7 @@ public class App { } if (username == null) { - var usernames = Manager.getAllLocalUsernames(dataPath); + var usernames = Manager.getAllLocalNumbers(dataPath); if (command instanceof MultiLocalCommand) { handleMultiLocalCommand((MultiLocalCommand) command, 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/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/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 338e70ac..f8c3c358 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -72,7 +72,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()); 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..49cd4719 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -116,7 +116,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"); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index 6ec8d964..be628bde 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -160,7 +160,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 e975a671..c8208774 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 getNumber() { + return m.getSelfNumber(); + } + @Override public void addDevice(String uri) { try { @@ -123,7 +127,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 +157,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 +209,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 +231,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 +245,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) { @@ -291,7 +295,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 +329,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 +345,13 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername())); + return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); } @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 +362,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 +371,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) { @@ -412,11 +416,7 @@ 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()); } } @@ -432,7 +432,7 @@ public class DbusSignalImpl implements Signal { if (avatar.isEmpty()) { avatar = null; } - final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername()); + 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()); @@ -573,10 +573,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()); @@ -589,16 +588,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); } @@ -639,7 +641,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 @@ -658,7 +660,7 @@ public class DbusSignalImpl implements Signal { if (group == null) { return false; } else { - return group.isMember(m.getSelfRecipientId()); + return group.isMember(); } } 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 18b38a2a..0a624e6b 100644 --- a/src/main/java/org/asamk/signal/util/CommandUtil.java +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -28,7 +28,7 @@ public class CommandUtil { 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) { From 593cd7d8ca6e8e0ab654accfd7e3c9d2ee01b001 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 18:51:44 +0200 Subject: [PATCH 7/8] Refactor dbus client mode to improve maintainability --- src/main/java/org/asamk/Signal.java | 2 +- src/main/java/org/asamk/signal/App.java | 13 +- .../asamk/signal/commands/DbusCommand.java | 20 - .../signal/commands/RemoteDeleteCommand.java | 46 +- .../asamk/signal/commands/SendCommand.java | 97 +--- .../signal/commands/SendReactionCommand.java | 53 +- .../signal/commands/UpdateGroupCommand.java | 43 +- .../exceptions/UserErrorException.java | 4 + .../asamk/signal/dbus/DbusManagerImpl.java | 487 ++++++++++++++++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 57 +- 10 files changed, 531 insertions(+), 291 deletions(-) delete mode 100644 src/main/java/org/asamk/signal/commands/DbusCommand.java create mode 100644 src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 2105ca76..cc521f6d 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -13,7 +13,7 @@ import java.util.List; */ public interface Signal extends DBusInterface { - String getNumber(); + String getSelfNumber(); long sendMessage( String message, List attachments, String recipient diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index e81b7018..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; @@ -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/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/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 f8c3c358..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() { @@ -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/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 49cd4719..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); @@ -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/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index c8208774..1a4fdc10 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -67,7 +67,7 @@ public class DbusSignalImpl implements Signal { } @Override - public String getNumber() { + public String getSelfNumber() { return m.getSelfNumber(); } @@ -96,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) { @@ -345,7 +343,8 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); + final var name = m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); + return name == null ? "" : name; } @Override @@ -403,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(); @@ -423,15 +422,9 @@ public class DbusSignalImpl implements Signal { @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; - } + 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)); @@ -499,17 +492,19 @@ public class DbusSignalImpl implements Signal { @Override public void updateProfile( - final String givenName, - final String familyName, - final String about, - final String aboutEmoji, + String givenName, + String familyName, + String about, + String aboutEmoji, String avatarPath, final boolean removeAvatar ) { try { - if (avatarPath.isEmpty()) { - avatarPath = null; - } + 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)); @@ -527,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 @@ -766,4 +751,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; + } } From f44b148946df5822f11a755ce3a4fba2d91d3b68 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 23:48:16 +0200 Subject: [PATCH 8/8] Allow message from pending member if it's just a group update Fixes #751 --- .../org/asamk/signal/manager/helper/IncomingMessageHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 45173da4..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 @@ -416,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; }