X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/404063a080a1a8784a1111304cc6b90b58362923..de2bfc7f7942908222ebcbac17e6072055acc062:/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java 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 708b0226..95f5bde4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2021 AsamK and contributors + Copyright (C) 2015-2022 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 @@ -16,6 +16,7 @@ */ package org.asamk.signal.manager; +import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.Configuration; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Group; @@ -24,14 +25,23 @@ import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.Pair; +import org.asamk.signal.manager.api.PendingAdminApprovalException; +import org.asamk.signal.manager.api.ReceiveConfig; +import org.asamk.signal.manager.api.Recipient; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResult; import org.asamk.signal.manager.api.SendMessageResults; +import org.asamk.signal.manager.api.StickerPackId; +import org.asamk.signal.manager.api.StickerPackInvalidException; +import org.asamk.signal.manager.api.StickerPackUrl; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UpdateGroup; +import org.asamk.signal.manager.api.UpdateProfile; +import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; @@ -39,55 +49,62 @@ 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.helper.AccountFileUpdater; import org.asamk.signal.manager.helper.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.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; -import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.asamk.signal.manager.storage.stickers.Sticker; -import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.asamk.signal.manager.storage.stickerPacks.JsonStickerPack; +import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore; +import org.asamk.signal.manager.storage.stickers.StickerPack; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.KeyUtils; +import org.asamk.signal.manager.util.MimeUtils; import org.asamk.signal.manager.util.StickerUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServicePreview; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; +import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.ServiceId; 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.internal.util.DynamicCredentialsProvider; +import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; +import java.io.ByteArrayInputStream; 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.time.Duration; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; -public class ManagerImpl implements Manager { +import io.reactivex.rxjava3.disposables.CompositeDisposable; + +class ManagerImpl implements Manager { private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); @@ -102,19 +119,18 @@ public class ManagerImpl implements Manager { private final Set weakHandlers = new HashSet<>(); private final Set messageHandlers = new HashSet<>(); private final List closedListeners = new ArrayList<>(); + private final List addressChangedListeners = new ArrayList<>(); + private final CompositeDisposable disposable = new CompositeDisposable(); ManagerImpl( SignalAccount account, PathConfig pathConfig, + AccountFileUpdater accountFileUpdater, ServiceEnvironmentConfig serviceEnvironmentConfig, String userAgent ) { this.account = account; - final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(), - account.getAccount(), - account.getPassword(), - account.getDeviceId()); final var sessionLock = new SignalSessionLock() { private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); @@ -126,15 +142,28 @@ public class ManagerImpl implements Manager { }; this.dependencies = new SignalDependencies(serviceEnvironmentConfig, userAgent, - credentialsProvider, - account.getSignalProtocolStore(), + account.getCredentialsProvider(), + account.getSignalServiceDataStore(), executor, sessionLock); final var avatarStore = new AvatarStore(pathConfig.avatarsPath()); final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath()); final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath()); - this.context = new Context(account, dependencies, avatarStore, attachmentStore, stickerPackStore); + this.context = new Context(account, new AccountFileUpdater() { + @Override + public void updateAccountIdentifiers(final String number, final ACI aci) { + accountFileUpdater.updateAccountIdentifiers(number, aci); + synchronized (addressChangedListeners) { + addressChangedListeners.forEach(Runnable::run); + } + } + + @Override + public void removeAccount() { + accountFileUpdater.removeAccount(); + } + }, dependencies, avatarStore, attachmentStore, stickerPackStore); this.context.getAccountHelper().setUnregisteredListener(this::close); this.context.getReceiveHelper().setAuthenticationFailureListener(this::close); this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> { @@ -142,23 +171,38 @@ public class ManagerImpl implements Manager { this.notifyAll(); } }); + disposable.add(account.getIdentityKeyStore().getIdentityChanges().subscribe(serviceId -> { + logger.trace("Archiving old sessions for {}", serviceId); + account.getAciSessionStore().archiveSessions(serviceId); + account.getPniSessionStore().archiveSessions(serviceId); + account.getSenderKeyStore().deleteSharedWith(serviceId); + final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); + final var profile = account.getProfileStore().getProfile(recipientId); + if (profile != null) { + account.getProfileStore() + .storeProfile(recipientId, + Profile.newBuilder(profile) + .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN) + .withLastUpdateTimestamp(0) + .build()); + } + })); } @Override public String getSelfNumber() { - return account.getAccount(); + return account.getNumber(); } - @Override - public void checkAccountState() throws IOException { + void checkAccountState() throws IOException { context.getAccountHelper().checkAccountState(); } @Override - public Map> areUsersRegistered(Set numbers) throws IOException { + public Map getUserStatus(Set numbers) throws IOException { final var canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { try { - final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getAccount()); + final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getNumber()); if (!canonicalizedNumber.equals(n)) { logger.debug("Normalized number {} to {}.", n, canonicalizedNumber); } @@ -177,8 +221,16 @@ public class ManagerImpl implements Manager { return numbers.stream().collect(Collectors.toMap(n -> n, n -> { final var number = canonicalizedNumbers.get(n); - final var aci = registeredUsers.get(number); - return new Pair<>(number.isEmpty() ? null : number, aci == null ? null : aci.uuid()); + final var user = registeredUsers.get(number); + final var serviceId = user == null ? null : user.getServiceId(); + final var profile = serviceId == null + ? null + : context.getProfileHelper() + .getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId)); + return new UserStatus(number.isEmpty() ? null : number, + serviceId == null ? null : serviceId.uuid(), + profile != null + && profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED); })); } @@ -188,6 +240,7 @@ public class ManagerImpl implements Manager { context.getAccountHelper().setDeviceName(deviceName); } context.getAccountHelper().updateAccountAttributes(); + context.getAccountHelper().checkWhoAmiI(); } @Override @@ -199,9 +252,9 @@ public class ManagerImpl implements Manager { @Override public void updateConfiguration( Configuration configuration - ) throws NotMasterDeviceException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); + ) throws NotPrimaryDeviceException { + if (!account.isPrimaryDevice()) { + throw new NotPrimaryDeviceException(); } final var configurationStore = account.getConfigurationStore(); @@ -221,15 +274,16 @@ public class ManagerImpl implements Manager { } @Override - public void setProfile( - String givenName, final String familyName, String about, String aboutEmoji, java.util.Optional avatar - ) throws IOException { + public void updateProfile(UpdateProfile updateProfile) throws IOException { context.getProfileHelper() - .setProfile(givenName, - familyName, - about, - aboutEmoji, - avatar == null ? null : Optional.fromNullable(avatar.orElse(null))); + .setProfile(updateProfile.getGivenName(), + updateProfile.getFamilyName(), + updateProfile.getAbout(), + updateProfile.getAboutEmoji(), + updateProfile.isDeleteAvatar() + ? Optional.empty() + : updateProfile.getAvatar() == null ? null : Optional.of(updateProfile.getAvatar()), + updateProfile.getMobileCoinAddress()); context.getSyncHelper().sendSyncFetchProfileMessage(); } @@ -254,7 +308,7 @@ public class ManagerImpl implements Manager { public List getLinkedDevices() throws IOException { var devices = dependencies.getAccountManager().getDevices(); account.setMultiDevice(devices.size() > 1); - var identityKey = account.getIdentityKeyPair().getPrivateKey(); + var identityKey = account.getAciIdentityKeyPair().getPrivateKey(); return devices.stream().map(d -> { String deviceName = d.getName(); if (deviceName != null) { @@ -273,7 +327,7 @@ public class ManagerImpl implements Manager { } @Override - public void removeLinkedDevices(long deviceId) throws IOException { + public void removeLinkedDevices(int deviceId) throws IOException { context.getAccountHelper().removeLinkedDevices(deviceId); } @@ -284,9 +338,9 @@ public class ManagerImpl implements Manager { } @Override - public void setRegistrationLockPin(java.util.Optional pin) throws IOException, NotMasterDeviceException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); + public void setRegistrationLockPin(Optional pin) throws IOException, NotPrimaryDeviceException { + if (!account.isPrimaryDevice()) { + throw new NotPrimaryDeviceException(); } if (pin.isPresent()) { context.getAccountHelper().setRegistrationPin(pin.get()); @@ -300,7 +354,7 @@ public class ManagerImpl implements Manager { } @Override - public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException { + public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { return context.getProfileHelper().getRecipientProfile(context.getRecipientHelper().resolveRecipient(recipient)); } @@ -314,9 +368,7 @@ public class ManagerImpl implements Manager { return null; } - return Group.from(groupInfo, - account.getRecipientStore()::resolveRecipientAddress, - account.getSelfRecipientId()); + return Group.from(groupInfo, account.getRecipientAddressResolver(), account.getSelfRecipientId()); } @Override @@ -329,6 +381,11 @@ public class ManagerImpl implements Manager { @Override public void deleteGroup(GroupId groupId) throws IOException { + final var group = context.getGroupHelper().getGroup(groupId); + if (group.isMember(account.getSelfRecipientId())) { + throw new IOException( + "The local group information cannot be removed, as the user is still a member of the group"); + } context.getGroupHelper().deleteGroup(groupId); } @@ -362,6 +419,12 @@ public class ManagerImpl implements Manager { updateGroup.getRemoveAdmins() == null ? null : context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveAdmins()), + updateGroup.getBanMembers() == null + ? null + : context.getRecipientHelper().resolveRecipients(updateGroup.getBanMembers()), + updateGroup.getUnbanMembers() == null + ? null + : context.getRecipientHelper().resolveRecipients(updateGroup.getUnbanMembers()), updateGroup.isResetGroupLink(), updateGroup.getGroupLinkState(), updateGroup.getAddMemberPermission(), @@ -374,7 +437,7 @@ public class ManagerImpl implements Manager { @Override public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl - ) throws IOException, InactiveGroupLinkException { + ) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException { return context.getGroupHelper().joinGroup(inviteLinkUrl); } @@ -406,9 +469,7 @@ public class ManagerImpl implements Manager { } private SendMessageResult toSendMessageResult(final org.whispersystems.signalservice.api.messages.SendMessageResult result) { - return SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress); + return SendMessageResult.from(result, account.getRecipientResolver(), account.getRecipientAddressResolver()); } private SendMessageResults sendTypingMessage( @@ -418,7 +479,7 @@ public class ManagerImpl implements Manager { final var timestamp = System.currentTimeMillis(); for (var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single single) { - final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty()); try { final var recipientId = context.getRecipientHelper().resolveRecipient(single); final var result = context.getSendHelper().sendTypingMessage(message, recipientId); @@ -447,7 +508,7 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendReadReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException { + ) { final var timestamp = System.currentTimeMillis(); var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, @@ -459,7 +520,7 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendViewedReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException { + ) { final var timestamp = System.currentTimeMillis(); var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, messageIds, @@ -472,7 +533,7 @@ public class ManagerImpl implements Manager { final RecipientIdentifier.Single sender, final long timestamp, final SignalServiceReceiptMessage receiptMessage - ) throws IOException { + ) { try { final var result = context.getSendHelper() .sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender)); @@ -487,6 +548,11 @@ public class ManagerImpl implements Manager { public SendMessageResults sendMessage( Message message, Set recipients ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException { + final var selfProfile = context.getProfileHelper().getSelfProfile(); + if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) { + logger.warn( + "No profile name set. When sending a message it's recommended to set a profile name with the updateProfile command. This may become mandatory in the future."); + } final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); @@ -495,10 +561,17 @@ public class ManagerImpl implements Manager { private void applyMessage( final SignalServiceDataMessage.Builder messageBuilder, final Message message ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException { - messageBuilder.withBody(message.messageText()); - final var attachments = message.attachments(); - if (attachments != null) { - messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(attachments)); + if (message.messageText().length() > 2000) { + final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8); + final var textAttachment = AttachmentUtils.createAttachmentStream(new StreamDetails(new ByteArrayInputStream( + messageBytes), MimeUtils.LONG_TEXT, messageBytes.length), Optional.empty()); + messageBuilder.withBody(message.messageText().substring(0, 2000)); + messageBuilder.withAttachment(textAttachment); + } else { + messageBuilder.withBody(message.messageText()); + } + if (message.attachments().size() > 0) { + messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(message.attachments())); } if (message.mentions().size() > 0) { messageBuilder.withMentions(resolveMentions(message.mentions())); @@ -507,10 +580,12 @@ public class ManagerImpl implements Manager { final var quote = message.quote().get(); messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(), context.getRecipientHelper() - .resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(quote.author())), + .resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(quote.author())) + .getServiceId(), quote.message(), List.of(), - resolveMentions(quote.mentions()))); + resolveMentions(quote.mentions()), + SignalServiceDataMessage.Quote.Type.NORMAL)); } if (message.sticker().isPresent()) { final var sticker = message.sticker().get(); @@ -518,10 +593,10 @@ public class ManagerImpl implements Manager { final var stickerId = sticker.stickerId(); final var stickerPack = context.getAccount().getStickerStore().getStickerPack(packId); - if (stickerPack == null || !context.getStickerPackStore().existsStickerPack(packId)) { + if (stickerPack == null) { throw new InvalidStickerException("Sticker pack not found"); } - final var manifest = context.getStickerPackStore().retrieveManifest(packId); + final var manifest = context.getStickerHelper().getOrRetrieveStickerPack(packId, stickerPack.packKey()); if (manifest.stickers().size() <= stickerId) { throw new InvalidStickerException("Sticker id not part of this pack"); } @@ -531,20 +606,41 @@ public class ManagerImpl implements Manager { throw new InvalidStickerException("Missing local sticker file"); } messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(), - stickerPack.getPackKey(), + stickerPack.packKey(), stickerId, manifestSticker.emoji(), - AttachmentUtils.createAttachment(streamDetails, Optional.absent()))); + AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty()))); + } + if (message.previews().size() > 0) { + final var previews = new ArrayList(message.previews().size()); + for (final var p : message.previews()) { + final var image = p.image().isPresent() ? context.getAttachmentHelper() + .uploadAttachment(p.image().get()) : null; + previews.add(new SignalServicePreview(p.url(), + p.title(), + p.description(), + 0, + Optional.ofNullable(image))); + } + messageBuilder.withPreviews(previews); + } + if (message.storyReply().isPresent()) { + final var storyReply = message.storyReply().get(); + final var authorServiceId = context.getRecipientHelper() + .resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(storyReply.author())) + .getServiceId(); + messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId, + storyReply.timestamp())); } } - private ArrayList resolveMentions(final List mentionList) throws IOException, UnregisteredRecipientException { + private ArrayList resolveMentions(final List mentionList) throws UnregisteredRecipientException { final var mentions = new ArrayList(); for (final var m : mentionList) { final var recipientId = context.getRecipientHelper().resolveRecipient(m.recipient()); mentions.add(new SignalServiceDataMessage.Mention(context.getRecipientHelper() .resolveSignalServiceAddress(recipientId) - .getAci(), m.start(), m.length())); + .getServiceId(), m.start(), m.length())); } return mentions; } @@ -555,6 +651,21 @@ public class ManagerImpl implements Manager { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single r) { + try { + final var recipientId = context.getRecipientHelper().resolveRecipient(r); + account.getMessageSendLogStore() + .deleteEntryForRecipientNonGroup(targetSentTimestamp, + account.getRecipientAddressResolver() + .resolveRecipientAddress(recipientId) + .getServiceId()); + } catch (UnregisteredRecipientException ignored) { + } + } else if (recipient instanceof RecipientIdentifier.Group r) { + account.getMessageSendLogStore().deleteEntryForGroup(targetSentTimestamp, r.groupId()); + } + } return sendMessage(messageBuilder, recipients); } @@ -564,17 +675,36 @@ public class ManagerImpl implements Manager { boolean remove, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, - Set recipients + Set recipients, + final boolean isStory ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor); - var reaction = new SignalServiceDataMessage.Reaction(emoji, - remove, - context.getRecipientHelper().resolveSignalServiceAddress(targetAuthorRecipientId), - targetSentTimestamp); + final var authorServiceId = context.getRecipientHelper() + .resolveSignalServiceAddress(targetAuthorRecipientId) + .getServiceId(); + var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, authorServiceId, targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); + if (isStory) { + messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId, + targetSentTimestamp)); + } return sendMessage(messageBuilder, recipients); } + @Override + public SendMessageResults sendPaymentNotificationMessage( + byte[] receipt, String note, RecipientIdentifier.Single recipient + ) throws IOException { + final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note); + final var payment = new SignalServiceDataMessage.Payment(paymentNotification); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withPayment(payment); + try { + return sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { + throw new AssertionError(e); + } + } + @Override public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); @@ -592,53 +722,90 @@ public class ManagerImpl implements Manager { } catch (UnregisteredRecipientException e) { continue; } - account.getSessionStore().deleteAllSessions(recipientId); + final var serviceId = context.getAccount() + .getRecipientAddressResolver() + .resolveRecipientAddress(recipientId) + .getServiceId(); + account.getAciSessionStore().deleteAllSessions(serviceId); } } } @Override public void deleteRecipient(final RecipientIdentifier.Single recipient) { - account.removeRecipient(account.getRecipientStore().resolveRecipient(recipient.toPartialRecipientAddress())); + account.removeRecipient(account.getRecipientResolver().resolveRecipient(recipient.getIdentifier())); } @Override public void deleteContact(final RecipientIdentifier.Single recipient) { account.getContactStore() - .deleteContact(account.getRecipientStore().resolveRecipient(recipient.toPartialRecipientAddress())); + .deleteContact(account.getRecipientResolver().resolveRecipient(recipient.getIdentifier())); } @Override public void setContactName( - RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException, IOException, UnregisteredRecipientException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); + RecipientIdentifier.Single recipient, String givenName, final String familyName + ) throws NotPrimaryDeviceException, UnregisteredRecipientException { + if (!account.isPrimaryDevice()) { + throw new NotPrimaryDeviceException(); } - context.getContactHelper().setContactName(context.getRecipientHelper().resolveRecipient(recipient), name); + context.getContactHelper() + .setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName); } @Override - public void setContactBlocked( - RecipientIdentifier.Single recipient, boolean blocked - ) throws NotMasterDeviceException, IOException, UnregisteredRecipientException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); + public void setContactsBlocked( + Collection recipients, boolean blocked + ) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException { + if (!account.isPrimaryDevice()) { + throw new NotPrimaryDeviceException(); + } + if (recipients.size() == 0) { + return; + } + final var recipientIds = context.getRecipientHelper().resolveRecipients(recipients); + final var selfRecipientId = account.getSelfRecipientId(); + boolean shouldRotateProfileKey = false; + for (final var recipientId : recipientIds) { + if (context.getContactHelper().isContactBlocked(recipientId) == blocked) { + continue; + } + context.getContactHelper().setContactBlocked(recipientId, blocked); + // if we don't have a common group with the blocked contact we need to rotate the profile key + shouldRotateProfileKey = blocked && ( + shouldRotateProfileKey || account.getGroupStore() + .getGroups() + .stream() + .noneMatch(g -> g.isMember(selfRecipientId) && g.isMember(recipientId)) + ); + } + if (shouldRotateProfileKey) { + context.getProfileHelper().rotateProfileKey(); } - context.getContactHelper().setContactBlocked(context.getRecipientHelper().resolveRecipient(recipient), blocked); - // TODO cycle our profile key, if we're not together in a group with recipient context.getSyncHelper().sendBlockedList(); } @Override - public void setGroupBlocked( - final GroupId groupId, final boolean blocked - ) throws GroupNotFoundException, NotMasterDeviceException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); + public void setGroupsBlocked( + final Collection groupIds, final boolean blocked + ) throws GroupNotFoundException, NotPrimaryDeviceException, IOException { + if (!account.isPrimaryDevice()) { + throw new NotPrimaryDeviceException(); + } + if (groupIds.size() == 0) { + return; + } + boolean shouldRotateProfileKey = false; + for (final var groupId : groupIds) { + if (context.getGroupHelper().isGroupBlocked(groupId) == blocked) { + continue; + } + context.getGroupHelper().setGroupBlocked(groupId, blocked); + shouldRotateProfileKey = blocked; + } + if (shouldRotateProfileKey) { + context.getProfileHelper().rotateProfileKey(); } - context.getGroupHelper().setGroupBlocked(groupId, blocked); - // TODO cycle our profile key context.getSyncHelper().sendBlockedList(); } @@ -657,7 +824,7 @@ public class ManagerImpl implements Manager { } @Override - public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { + public StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var messageSender = dependencies.getMessageSender(); @@ -666,20 +833,33 @@ public class ManagerImpl implements Manager { var packIdString = messageSender.uploadStickerManifest(manifest, packKey); var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString)); - var sticker = new Sticker(packId, packKey); - account.getStickerStore().updateSticker(sticker); + var sticker = new StickerPack(packId, packKey); + account.getStickerStore().addStickerPack(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); - } + return new StickerPackUrl(packId, packKey); + } + + @Override + public List getStickerPacks() { + final var stickerPackStore = context.getStickerPackStore(); + return account.getStickerStore().getStickerPacks().stream().map(pack -> { + if (stickerPackStore.existsStickerPack(pack.packId())) { + try { + final var manifest = stickerPackStore.retrieveManifest(pack.packId()); + return new org.asamk.signal.manager.api.StickerPack(pack.packId(), + new StickerPackUrl(pack.packId(), pack.packKey()), + pack.isInstalled(), + manifest.title(), + manifest.author(), + Optional.ofNullable(manifest.cover() == null ? null : manifest.cover().toApi()), + manifest.stickers().stream().map(JsonStickerPack.JsonSticker::toApi).toList()); + } catch (Exception e) { + logger.warn("Failed to read local sticker pack manifest: {}", e.getMessage(), e); + } + } + + return new org.asamk.signal.manager.api.StickerPack(pack.packId(), pack.packKey(), pack.isInstalled()); + }).toList(); } @Override @@ -689,9 +869,7 @@ public class ManagerImpl implements Manager { } void retrieveRemoteStorage() throws IOException { - if (account.getStorageKey() != null) { - context.getStorageHelper().readDataFromStorage(); - } + context.getStorageHelper().readDataFromStorage(); } @Override @@ -709,30 +887,26 @@ public class ManagerImpl implements Manager { } } + private static final AtomicInteger threadNumber = new AtomicInteger(0); + private void startReceiveThreadIfRequired() { if (receiveThread != null) { return; } receiveThread = new Thread(() -> { logger.debug("Starting receiving messages"); - while (!Thread.interrupted()) { - try { - context.getReceiveHelper().receiveMessages(Duration.ofMinutes(1), false, (envelope, e) -> { - synchronized (messageHandlers) { - Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> { - try { - h.handleMessage(envelope, e); - } catch (Exception ex) { - logger.warn("Message handler failed, ignoring", ex); - } - }); + context.getReceiveHelper().receiveMessagesContinuously((envelope, e) -> { + synchronized (messageHandlers) { + final var handlers = Stream.concat(messageHandlers.stream(), weakHandlers.stream()).toList(); + handlers.forEach(h -> { + try { + h.handleMessage(envelope, e); + } catch (Throwable ex) { + logger.warn("Message handler failed, ignoring", ex); } }); - break; - } catch (IOException e) { - logger.warn("Receiving messages failed, retrying", e); } - } + }); logger.debug("Finished receiving messages"); synchronized (messageHandlers) { receiveThread = null; @@ -744,6 +918,7 @@ public class ManagerImpl implements Manager { } } }); + receiveThread.setName("receive-" + threadNumber.getAndIncrement()); receiveThread.start(); } @@ -765,7 +940,10 @@ public class ManagerImpl implements Manager { } private void stopReceiveThread(final Thread thread) { - thread.interrupt(); + if (context.getReceiveHelper().requestStopReceiveMessages()) { + logger.debug("Receive stop requested, interrupting read from server."); + thread.interrupt(); + } try { thread.join(); } catch (InterruptedException ignored) { @@ -783,17 +961,16 @@ public class ManagerImpl implements Manager { } @Override - public void receiveMessages(Duration timeout, ReceiveMessageHandler handler) throws IOException { - receiveMessages(timeout, true, handler); - } - - @Override - public void receiveMessages(ReceiveMessageHandler handler) throws IOException { - receiveMessages(Duration.ofMinutes(1), false, handler); + public void receiveMessages( + Optional timeout, + Optional maxMessages, + ReceiveMessageHandler handler + ) throws IOException { + receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler); } private void receiveMessages( - Duration timeout, boolean returnOnTimeout, ReceiveMessageHandler handler + Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler ) throws IOException { if (isReceiving()) { throw new IllegalStateException("Already receiving message."); @@ -801,7 +978,7 @@ public class ManagerImpl implements Manager { isReceivingSynchronous = true; receiveThread = Thread.currentThread(); try { - context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, handler); + context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, maxMessages, handler); } finally { receiveThread = null; isReceivingSynchronous = false; @@ -809,8 +986,8 @@ public class ManagerImpl implements Manager { } @Override - public void setIgnoreAttachments(final boolean ignoreAttachments) { - context.getReceiveHelper().setIgnoreAttachments(ignoreAttachments); + public void setReceiveConfig(final ReceiveConfig receiveConfig) { + context.getReceiveHelper().setReceiveConfig(receiveConfig); } @Override @@ -823,7 +1000,7 @@ public class ManagerImpl implements Manager { final RecipientId recipientId; try { recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException | UnregisteredRecipientException e) { + } catch (UnregisteredRecipientException e) { return false; } return context.getContactHelper().isContactBlocked(recipientId); @@ -835,11 +1012,33 @@ public class ManagerImpl implements Manager { } @Override - public List> getContacts() { - return account.getContactStore() - .getContacts() + public List getRecipients( + boolean onlyContacts, + Optional blocked, + Collection recipients, + Optional name + ) { + final var recipientIds = recipients.stream().map(a -> { + try { + return context.getRecipientHelper().resolveRecipient(a); + } catch (UnregisteredRecipientException e) { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toSet()); + if (!recipients.isEmpty() && recipientIds.isEmpty()) { + return List.of(); + } + // refresh profiles of explicitly given recipients + context.getProfileHelper().refreshRecipientProfiles(recipientIds); + return account.getRecipientStore() + .getRecipients(onlyContacts, blocked, recipientIds, name) .stream() - .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second())) + .map(s -> new Recipient(s.getRecipientId(), + s.getAddress().toApiRecipientAddress(), + s.getContact(), + s.getProfileKey(), + s.getExpiringProfileKeyCredential(), + s.getProfile())) .toList(); } @@ -848,7 +1047,7 @@ public class ManagerImpl implements Manager { final RecipientId recipientId; try { recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException | UnregisteredRecipientException e) { + } catch (UnregisteredRecipientException e) { return null; } @@ -880,27 +1079,30 @@ public class ManagerImpl implements Manager { return null; } - final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); + final var address = account.getRecipientAddressResolver() + .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(identityInfo.getServiceId())); final var scannableFingerprint = context.getIdentityHelper() - .computeSafetyNumberForScanning(identityInfo.getRecipientId(), identityInfo.getIdentityKey()); - return new Identity(address, + .computeSafetyNumberForScanning(identityInfo.getServiceId(), identityInfo.getIdentityKey()); + return new Identity(address.toApiRecipientAddress(), identityInfo.getIdentityKey(), context.getIdentityHelper() - .computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), + .computeSafetyNumber(identityInfo.getServiceId(), identityInfo.getIdentityKey()), scannableFingerprint == null ? null : scannableFingerprint.getSerialized(), identityInfo.getTrustLevel(), - identityInfo.getDateAdded()); + identityInfo.getDateAddedTimestamp()); } @Override public List getIdentities(RecipientIdentifier.Single recipient) { - IdentityInfo identity; + ServiceId serviceId; try { - identity = account.getIdentityKeyStore() - .getIdentity(context.getRecipientHelper().resolveRecipient(recipient)); - } catch (IOException | UnregisteredRecipientException e) { - identity = null; + serviceId = account.getRecipientAddressResolver() + .resolveRecipientAddress(context.getRecipientHelper().resolveRecipient(recipient)) + .getServiceId(); + } catch (UnregisteredRecipientException e) { + return List.of(); } + final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId); return identity == null ? List.of() : List.of(toIdentity(identity)); } @@ -935,12 +1137,7 @@ public class ManagerImpl implements Manager { private boolean trustIdentity( RecipientIdentifier.Single recipient, Function trustMethod ) throws UnregisteredRecipientException { - RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException e) { - return false; - } + final var recipientId = context.getRecipientHelper().resolveRecipient(recipient); final var updated = trustMethod.apply(recipientId); if (updated && this.isReceiving()) { context.getReceiveHelper().setNeedsToRetryFailedMessages(true); @@ -948,6 +1145,13 @@ public class ManagerImpl implements Manager { return updated; } + @Override + public void addAddressChangedListener(final Runnable listener) { + synchronized (addressChangedListeners) { + addressChangedListeners.add(listener); + } + } + @Override public void addClosedListener(final Runnable listener) { synchronized (closedListeners) { @@ -970,15 +1174,17 @@ public class ManagerImpl implements Manager { executor.shutdown(); dependencies.getSignalWebSocket().disconnect(); + disposable.dispose(); + + if (account != null) { + account.close(); + } synchronized (closedListeners) { closedListeners.forEach(Runnable::run); closedListeners.clear(); } - if (account != null) { - account.close(); - } account = null; } }