X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/f5e5dd551d853778b78429a537fb70e187765563..22add1cbee357e988ca9c85d9d161fc783fd5fa3:/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 1f731ee8..fa041c55 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,18 +16,25 @@ */ 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; import org.asamk.signal.manager.api.Identity; 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.NotMasterDeviceException; import org.asamk.signal.manager.api.Pair; 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.StickerPack; +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; @@ -38,6 +45,7 @@ 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; @@ -46,81 +54,76 @@ 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.stickerPacks.JsonStickerPack; +import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore; import org.asamk.signal.manager.storage.stickers.Sticker; -import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.ECPublicKey; -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.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.ACI; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; 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.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.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +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.TimeUnit; +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; -import static org.asamk.signal.manager.config.ServiceConfig.capabilities; +import io.reactivex.rxjava3.disposables.CompositeDisposable; -public class ManagerImpl implements Manager { +class ManagerImpl implements Manager { private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); - private final SignalDependencies dependencies; - private SignalAccount account; + private AccountFileUpdater accountFileUpdater; + private final SignalDependencies dependencies; + private final Context context; private final ExecutorService executor = Executors.newCachedThreadPool(); - private final Context context; - private Thread receiveThread; + private boolean isReceivingSynchronous; private final Set weakHandlers = new HashSet<>(); private final Set messageHandlers = new HashSet<>(); private final List closedListeners = new ArrayList<>(); - private boolean isReceivingSynchronous; + 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; + this.accountFileUpdater = accountFileUpdater; - 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(); @@ -132,71 +135,65 @@ 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.getReceiveHelper().setAuthenticationFailureListener(() -> { - try { - close(); - } catch (IOException e) { - logger.warn("Failed to close account after authentication failure", e); + 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(() -> { synchronized (this) { this.notifyAll(); } }); + disposable.add(account.getIdentityKeyStore().getIdentityChanges().subscribe(recipientId -> { + logger.trace("Archiving old sessions for {}", recipientId); + account.getSessionStore().archiveSessions(recipientId); + account.getSenderKeyStore().deleteSharedWith(recipientId); + final var profile = account.getRecipientStore().getProfile(recipientId); + if (profile != null) { + account.getRecipientStore() + .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 { - 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); - } - } - try { - context.getPreKeyHelper().refreshPreKeysIfNecessary(); - if (account.getAci() == null) { - account.setAci(ACI.parseOrNull(dependencies.getAccountManager().getWhoAmI().getAci())); - } - updateAccountAttributes(null); - } catch (AuthorizationFailedException e) { - account.setRegistered(false); - throw e; - } + void checkAccountState() throws IOException { + context.getAccountHelper().checkAccountState(); } - /** - * 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 it's unable to get the contacts to check if they're registered - */ @Override public Map> areUsersRegistered(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); } @@ -222,34 +219,16 @@ public class ManagerImpl implements Manager { @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); + if (deviceName != null) { + context.getAccountHelper().setDeviceName(deviceName); } - dependencies.getAccountManager() - .setAccountAttributes(encryptedDeviceName, - null, - account.getLocalRegistrationId(), - true, - null, - account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - capabilities, - account.isDiscoverableByPhoneNumber()); + context.getAccountHelper().updateAccountAttributes(); } @Override public Configuration getConfiguration() { final var configurationStore = account.getConfigurationStore(); - return new Configuration(java.util.Optional.ofNullable(configurationStore.getReadReceipts()), - java.util.Optional.ofNullable(configurationStore.getUnidentifiedDeliveryIndicators()), - java.util.Optional.ofNullable(configurationStore.getTypingIndicators()), - java.util.Optional.ofNullable(configurationStore.getLinkPreviews())); + return Configuration.from(configurationStore); } @Override @@ -276,50 +255,22 @@ public class ManagerImpl implements Manager { context.getSyncHelper().sendConfigurationMessage(); } - /** - * @param givenName if null, the previous givenName will be kept - * @param familyName if null, the previous familyName will be kept - * @param about if null, the previous about text will be kept - * @param aboutEmoji if null, the previous about emoji will be kept - * @param avatar if avatar is null the image from the local avatar store is used (if present), - */ @Override public void setProfile( - String givenName, final String familyName, String about, String aboutEmoji, java.util.Optional avatar + String givenName, final String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException { - context.getProfileHelper() - .setProfile(givenName, - familyName, - about, - aboutEmoji, - avatar == null ? null : Optional.fromNullable(avatar.orElse(null))); + context.getProfileHelper().setProfile(givenName, familyName, about, aboutEmoji, avatar); context.getSyncHelper().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); - close(); + context.getAccountHelper().unregister(); } @Override public void deleteAccount() throws IOException { - try { - context.getPinHelper().removeRegistrationLockPin(); - } catch (IOException e) { - logger.warn("Failed to remove registration lock pin"); - } - account.setRegistrationLockPin(null, null); - - dependencies.getAccountManager().deleteAccount(); - - account.setRegistered(false); - close(); + context.getAccountHelper().deleteAccount(); } @Override @@ -333,7 +284,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) { @@ -352,56 +303,25 @@ public class ManagerImpl implements Manager { } @Override - public void removeLinkedDevices(long deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); + public void removeLinkedDevices(int deviceId) throws IOException { + context.getAccountHelper().removeLinkedDevices(deviceId); } @Override public void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException { - var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); - - addDevice(info.deviceIdentifier(), info.deviceKey()); - } - - private void addDevice( - String deviceIdentifier, ECPublicKey deviceKey - ) throws IOException, InvalidDeviceLinkException { - var identityKeyPair = account.getIdentityKeyPair(); - var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); - - try { - dependencies.getAccountManager() - .addDevice(deviceIdentifier, - deviceKey, - identityKeyPair, - Optional.of(account.getProfileKey().serialize()), - verificationCode); - } catch (InvalidKeyException e) { - throw new InvalidDeviceLinkException("Invalid device link", e); - } - account.setMultiDevice(true); + var deviceLinkInfo = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + context.getAccountHelper().addDevice(deviceLinkInfo); } @Override - public void setRegistrationLockPin(java.util.Optional pin) throws IOException { + public void setRegistrationLockPin(Optional pin) throws IOException, NotMasterDeviceException { if (!account.isMasterDevice()) { - throw new RuntimeException("Only master device can set a PIN"); + throw new NotMasterDeviceException(); } if (pin.isPresent()) { - final var masterKey = account.getPinMasterKey() != null - ? account.getPinMasterKey() - : KeyUtils.createMasterKey(); - - context.getPinHelper().setRegistrationLockPin(pin.get(), masterKey); - - account.setRegistrationLockPin(pin.get(), masterKey); + context.getAccountHelper().setRegistrationPin(pin.get()); } else { - // Remove KBS Pin - context.getPinHelper().removeRegistrationLockPin(); - - account.setRegistrationLockPin(null, null); + context.getAccountHelper().removeRegistrationPin(); } } @@ -424,33 +344,9 @@ public class ManagerImpl implements Manager { 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.getMessageExpirationTimer(), - groupInfo.getPermissionAddMember(), - groupInfo.getPermissionEditDetails(), - groupInfo.getPermissionSendMessage(), - groupInfo.isMember(account.getSelfRecipientId()), - groupInfo.isAdmin(account.getSelfRecipientId())); + return Group.from(groupInfo, + account.getRecipientStore()::resolveRecipientAddress, + account.getSelfRecipientId()); } @Override @@ -463,6 +359,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); } @@ -496,6 +397,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(), @@ -523,33 +430,28 @@ public class ManagerImpl implements Manager { try { final var recipientId = context.getRecipientHelper().resolveRecipient(single); final var result = context.getSendHelper().sendMessage(messageBuilder, recipientId); - results.put(recipient, - List.of(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress))); + results.put(recipient, List.of(toSendMessageResult(result))); } catch (UnregisteredRecipientException e) { results.put(recipient, List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress()))); } } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { final var result = context.getSendHelper().sendSelfMessage(messageBuilder); - results.put(recipient, - List.of(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress))); + results.put(recipient, List.of(toSendMessageResult(result))); } else if (recipient instanceof RecipientIdentifier.Group group) { final var result = context.getSendHelper().sendAsGroupMessage(messageBuilder, group.groupId()); - results.put(recipient, - result.stream() - .map(sendMessageResult -> SendMessageResult.from(sendMessageResult, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress)) - .toList()); + results.put(recipient, result.stream().map(this::toSendMessageResult).toList()); } } return new SendMessageResults(timestamp, results); } + private SendMessageResult toSendMessageResult(final org.whispersystems.signalservice.api.messages.SendMessageResult result) { + return SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress); + } + private SendMessageResults sendTypingMessage( SignalServiceTypingMessage.Action action, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { @@ -557,14 +459,11 @@ 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); - results.put(recipient, - List.of(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress))); + results.put(recipient, List.of(toSendMessageResult(result))); } catch (UnregisteredRecipientException e) { results.put(recipient, List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress()))); @@ -573,12 +472,7 @@ public class ManagerImpl implements Manager { final var groupId = ((RecipientIdentifier.Group) recipient).groupId(); final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); final var result = context.getSendHelper().sendGroupTypingMessage(message, groupId); - results.put(recipient, - result.stream() - .map(r -> SendMessageResult.from(r, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress)) - .toList()); + results.put(recipient, result.stream().map(this::toSendMessageResult).toList()); } } return new SendMessageResults(timestamp, results); @@ -623,11 +517,7 @@ public class ManagerImpl implements Manager { try { final var result = context.getSendHelper() .sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender)); - return new SendMessageResults(timestamp, - Map.of(sender, - List.of(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress)))); + return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result)))); } catch (UnregisteredRecipientException e) { return new SendMessageResults(timestamp, Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress())))); @@ -637,7 +527,12 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + ) 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 wit the updateProfile command. This may become mandatory in the future."); + } final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); @@ -645,7 +540,7 @@ public class ManagerImpl implements Manager { private void applyMessage( final SignalServiceDataMessage.Builder messageBuilder, final Message message - ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException { + ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException { messageBuilder.withBody(message.messageText()); final var attachments = message.attachments(); if (attachments != null) { @@ -661,7 +556,32 @@ public class ManagerImpl implements Manager { .resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(quote.author())), quote.message(), List.of(), - resolveMentions(quote.mentions()))); + resolveMentions(quote.mentions()), + SignalServiceDataMessage.Quote.Type.NORMAL)); + } + if (message.sticker().isPresent()) { + final var sticker = message.sticker().get(); + final var packId = StickerPackId.deserialize(sticker.packId()); + final var stickerId = sticker.stickerId(); + + final var stickerPack = context.getAccount().getStickerStore().getStickerPack(packId); + if (stickerPack == null) { + throw new InvalidStickerException("Sticker pack not found"); + } + final var manifest = context.getStickerHelper().getOrRetrieveStickerPack(packId, stickerPack.getPackKey()); + if (manifest.stickers().size() <= stickerId) { + throw new InvalidStickerException("Sticker id not part of this pack"); + } + final var manifestSticker = manifest.stickers().get(stickerId); + final var streamDetails = context.getStickerPackStore().retrieveSticker(packId, stickerId); + if (streamDetails == null) { + throw new InvalidStickerException("Missing local sticker file"); + } + messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(), + stickerPack.getPackKey(), + stickerId, + manifestSticker.emoji(), + AttachmentUtils.createAttachment(streamDetails, Optional.empty()))); } } @@ -671,7 +591,7 @@ public class ManagerImpl implements Manager { 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; } @@ -682,6 +602,17 @@ 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, recipientId); + } catch (UnregisteredRecipientException ignored) { + } + } else if (recipient instanceof RecipientIdentifier.Group r) { + account.getMessageSendLogStore().deleteEntryForGroup(targetSentTimestamp, r.groupId()); + } + } return sendMessage(messageBuilder, recipients); } @@ -769,9 +700,6 @@ public class ManagerImpl implements Manager { context.getSyncHelper().sendBlockedList(); } - /** - * Change the expiration timer for a contact - */ @Override public void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer @@ -786,14 +714,8 @@ public class ManagerImpl implements Manager { } } - /** - * 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 { + public StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var messageSender = dependencies.getMessageSender(); @@ -805,17 +727,30 @@ public class ManagerImpl implements Manager { 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); - } + 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.getPackId())) { + try { + final var manifest = stickerPackStore.retrieveManifest(pack.getPackId()); + return new StickerPack(pack.getPackId(), + new StickerPackUrl(pack.getPackId(), pack.getPackKey()), + 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 StickerPack(pack.getPackId(), pack.getPackKey(), pack.isInstalled()); + }).toList(); } @Override @@ -845,30 +780,25 @@ 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) { + Stream.concat(messageHandlers.stream(), weakHandlers.stream()).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; @@ -880,6 +810,7 @@ public class ManagerImpl implements Manager { } } }); + receiveThread.setName("receive-" + threadNumber.getAndIncrement()); receiveThread.start(); } @@ -901,7 +832,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) { @@ -1040,95 +974,57 @@ public class ManagerImpl implements Manager { return identity == null ? List.of() : List.of(toIdentity(identity)); } - /** - * Trust this the identity with this fingerprint - * - * @param recipient account of the identity - * @param fingerprint Fingerprint - */ @Override public boolean trustIdentityVerified( RecipientIdentifier.Single recipient, byte[] fingerprint ) throws UnregisteredRecipientException { - RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - final var updated = context.getIdentityHelper().trustIdentityVerified(recipientId, fingerprint); - if (updated && this.isReceiving()) { - context.getReceiveHelper().setNeedsToRetryFailedMessages(true); - } - return updated; + return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityVerified(r, fingerprint)); } - /** - * Trust this the identity with this safety number - * - * @param recipient account of the identity - * @param safetyNumber Safety number - */ @Override public boolean trustIdentityVerifiedSafetyNumber( RecipientIdentifier.Single recipient, String safetyNumber ) throws UnregisteredRecipientException { - RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - final var updated = context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); - if (updated && this.isReceiving()) { - context.getReceiveHelper().setNeedsToRetryFailedMessages(true); - } - return updated; + return trustIdentity(recipient, + r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber)); } - /** - * Trust this the identity with this scannable safety number - * - * @param recipient account of the identity - * @param safetyNumber Scannable safety number - */ @Override public boolean trustIdentityVerifiedSafetyNumber( RecipientIdentifier.Single recipient, byte[] safetyNumber ) throws UnregisteredRecipientException { - RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - final var updated = context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); - if (updated && this.isReceiving()) { - context.getReceiveHelper().setNeedsToRetryFailedMessages(true); - } - return updated; + return trustIdentity(recipient, + r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber)); } - /** - * Trust all keys of this identity without verification - * - * @param recipient account of the identity - */ @Override public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { + return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityAllKeys(r)); + } + + 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 updated = context.getIdentityHelper().trustIdentityAllKeys(recipientId); + final var updated = trustMethod.apply(recipientId); if (updated && this.isReceiving()) { context.getReceiveHelper().setNeedsToRetryFailedMessages(true); } return updated; } + @Override + public void addAddressChangedListener(final Runnable listener) { + synchronized (addressChangedListeners) { + addressChangedListeners.add(listener); + } + } + @Override public void addClosedListener(final Runnable listener) { synchronized (closedListeners) { @@ -1137,7 +1033,7 @@ public class ManagerImpl implements Manager { } @Override - public void close() throws IOException { + public void close() { Thread thread; synchronized (messageHandlers) { weakHandlers.clear(); @@ -1151,15 +1047,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; } }