X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/3de30e166f4eb6be0cd10548ed2c6cce2fe2282e..a8bbdb54d006f157a009ece0cae5bf72fb636ced:/lib/src/main/java/org/asamk/signal/manager/Manager.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 b003279a..98b02c7f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -17,19 +17,26 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.Device; +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.GroupIdV1; 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.GroupUtils; +import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.jobs.Job; +import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; @@ -84,6 +91,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalSessionLock; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; @@ -102,6 +110,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; @@ -118,6 +127,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; @@ -195,6 +205,7 @@ public class Manager implements Closeable { private final PinHelper pinHelper; private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; + private final StickerPackStore stickerPackStore; private final SignalSessionLock sessionLock = new SignalSessionLock() { private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); @@ -268,6 +279,7 @@ public class Manager implements Closeable { this::resolveSignalServiceAddress); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); } public String getUsername() { @@ -327,6 +339,17 @@ public class Manager implements Closeable { } public void checkAccountState() throws IOException { + if (account.getLastReceiveTimestamp() == 0) { + logger.warn("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); + } + } if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { refreshPreKeys(); } @@ -571,7 +594,7 @@ public class Manager implements Closeable { ) { var profile = account.getProfileStore().getProfile(recipientId); - var now = new Date().getTime(); + var now = System.currentTimeMillis(); // Profiles are cached for 24h before retrieving them again, unless forced if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) { return profile; @@ -595,21 +618,27 @@ public class Manager implements Closeable { return null; } + profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile); + account.getProfileStore().storeProfile(recipientId, profile); + + return profile; + } + + private Profile decryptProfileIfKeyKnown( + final RecipientId recipientId, final SignalServiceProfile encryptedProfile + ) { var profileKey = account.getProfileStore().getProfileKey(recipientId); if (profileKey == null) { - profile = new Profile(new Date().getTime(), + return new Profile(System.currentTimeMillis(), null, null, null, null, ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), ProfileUtils.getCapabilities(encryptedProfile)); - } else { - profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); } - account.getProfileStore().storeProfile(recipientId, profile); - return profile; + return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); } private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) { @@ -638,7 +667,7 @@ public class Manager implements Closeable { } } catch (InvalidKeyException ignored) { logger.warn("Got invalid identity key in profile for {}", - resolveSignalServiceAddress(recipientId).getLegacyIdentifier()); + resolveSignalServiceAddress(recipientId).getIdentifier()); } return profileAndCredential; } @@ -761,25 +790,58 @@ public class Manager implements Closeable { return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); } - public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { - SignalServiceDataMessage.Builder messageBuilder; + public Pair> sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { + var group = getGroupForUpdating(groupId); + if (group instanceof GroupInfoV1) { + return quitGroupV1((GroupInfoV1) group); + } - final var g = getGroupForUpdating(groupId); - if (g instanceof GroupInfoV1) { - var groupInfoV1 = (GroupInfoV1) g; - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build(); - messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); - groupInfoV1.removeMember(account.getSelfRecipientId()); - account.getGroupStore().updateGroup(groupInfoV1); - } else { - final var groupInfoV2 = (GroupInfoV2) g; - final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2); - groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); - messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); - account.getGroupStore().updateGroup(groupInfoV2); + final var newAdmins = getSignalServiceAddresses(groupAdmins); + try { + return quitGroupV2((GroupInfoV2) group, newAdmins); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return quitGroupV2((GroupInfoV2) group, newAdmins); } + } - return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); + private Pair> quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) + .withId(groupInfoV1.getGroupId().serialize()) + .build(); + + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); + groupInfoV1.removeMember(account.getSelfRecipientId()); + account.getGroupStore().updateGroup(groupInfoV1); + return sendMessage(messageBuilder, groupInfoV1.getMembersWithout(account.getSelfRecipientId())); + } + + private Pair> quitGroupV2( + final GroupInfoV2 groupInfoV2, final Set newAdmins + ) throws LastGroupAdminException, IOException { + final var currentAdmins = groupInfoV2.getAdminMembers(); + newAdmins.removeAll(currentAdmins); + newAdmins.retainAll(groupInfoV2.getMembers()); + if (currentAdmins.contains(getSelfRecipientId()) + && currentAdmins.size() == 1 + && groupInfoV2.getMembers().size() > 1 + && newAdmins.size() == 0) { + // Last admin can't leave the group, unless she's also the last member + throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle()); + } + final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); + groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); + var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + account.getGroupStore().updateGroup(groupInfoV2); + return sendMessage(messageBuilder, groupInfoV2.getMembersWithout(account.getSelfRecipientId())); + } + + public void deleteGroup(GroupId groupId) throws IOException { + account.getGroupStore().deleteGroup(groupId); + avatarStore.deleteGroupAvatar(groupId); } public Pair> createGroup( @@ -833,7 +895,12 @@ public class Manager implements Closeable { List removeMembers, List admins, List removeAdmins, - File avatarFile + boolean resetGroupLink, + GroupLinkState groupLinkState, + GroupPermission addMemberPermission, + GroupPermission editDetailsPermission, + File avatarFile, + Integer expirationTimer ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return updateGroup(groupId, name, @@ -842,7 +909,12 @@ public class Manager implements Closeable { removeMembers == null ? null : getSignalServiceAddresses(removeMembers), admins == null ? null : getSignalServiceAddresses(admins), removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins), - avatarFile); + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer); } private Pair> updateGroup( @@ -853,22 +925,55 @@ public class Manager implements Closeable { final Set removeMembers, final Set admins, final Set removeAdmins, - final File avatarFile + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + final Integer expirationTimer ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV2) { - return updateGroupV2((GroupInfoV2) group, - name, - description, - members, - removeMembers, - admins, - removeAdmins, - avatarFile); + try { + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer); + } } - return updateGroupV1((GroupInfoV1) group, name, members, avatarFile); + final var gv1 = (GroupInfoV1) group; + final var result = updateGroupV1(gv1, name, members, avatarFile); + if (expirationTimer != null) { + setExpirationTimer(gv1, expirationTimer); + } + return result; } private Pair> updateGroupV1( @@ -928,7 +1033,12 @@ public class Manager implements Closeable { final Set removeMembers, final Set admins, final Set removeAdmins, - final File avatarFile + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + Integer expirationTimer ) throws IOException { Pair> result = null; if (group.isPendingMember(account.getSelfRecipientId())) { @@ -989,7 +1099,32 @@ public class Manager implements Closeable { } } - if (result == null || name != null || description != null || avatarFile != null) { + if (resetGroupLink) { + var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (groupLinkState != null) { + var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (addMemberPermission != null) { + var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (editDetailsPermission != null) { + var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (expirationTimer != null) { + var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (name != null || description != null || avatarFile != null) { var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); if (avatarFile != null) { avatarStore.storeGroupAvatar(group.getGroupId(), @@ -1285,15 +1420,17 @@ public class Manager implements Closeable { /** * Change the expiration timer for a group */ - public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) { - var g = getGroup(groupId); - if (g instanceof GroupInfoV1) { - var groupInfoV1 = (GroupInfoV1) g; - groupInfoV1.messageExpirationTime = messageExpirationTimer; - account.getGroupStore().updateGroup(groupInfoV1); - } else { - throw new RuntimeException("TODO Not implemented!"); - } + private void setExpirationTimer( + GroupInfoV1 groupInfoV1, int messageExpirationTimer + ) throws NotAGroupMemberException, GroupNotFoundException, IOException { + groupInfoV1.messageExpirationTime = messageExpirationTimer; + account.getGroupStore().updateGroup(groupInfoV1); + sendExpirationTimerUpdate(groupInfoV1.getGroupId()); + } + + private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + sendGroupMessage(messageBuilder, groupId); } /** @@ -1308,18 +1445,20 @@ public class Manager implements Closeable { var messageSender = createMessageSender(); var packKey = KeyUtils.createStickerUploadKey(); - var packId = messageSender.uploadStickerManifest(manifest, packKey); + var packIdString = messageSender.uploadStickerManifest(manifest, packKey); + var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString)); - var sticker = new Sticker(StickerPackId.deserialize(Hex.fromStringCondensed(packId)), packKey); + var sticker = new Sticker(packId, packKey); account.getStickerStore().updateSticker(sticker); try { return new URI("https", "signal.art", "/addstickers/", - "pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode( - Hex.toStringCondensed(packKey), - StandardCharsets.UTF_8)).toString(); + "pack_id=" + + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + + "&pack_key=" + + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString(); } catch (URISyntaxException e) { throw new AssertionError(e); } @@ -1411,7 +1550,7 @@ public class Manager implements Closeable { private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { var messageSender = createMessageSender(); - messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync()); + messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync()); } private Set getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { @@ -1473,6 +1612,40 @@ public class Manager implements Closeable { } } + public void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, InvalidNumberException { + sendTypingMessageInternal(action, getSignalServiceAddresses(recipients)); + } + + private void sendTypingMessageInternal( + TypingAction action, Set recipientIds + ) throws IOException, UntrustedIdentityException { + final var timestamp = System.currentTimeMillis(); + var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent()); + var messageSender = createMessageSender(); + for (var recipientId : recipientIds) { + final var address = resolveSignalServiceAddress(recipientId); + messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + } + } + + public void sendGroupTypingMessage( + TypingAction action, GroupId groupId + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var timestamp = System.currentTimeMillis(); + final var g = getGroupForSending(groupId); + final var message = new SignalServiceTypingMessage(action.toSignalService(), + timestamp, + Optional.of(groupId.serialize())); + final var messageSender = createMessageSender(); + final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId())); + final var addresses = recipientIdList.stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toList()); + messageSender.sendTyping(addresses, unidentifiedAccessHelper.getAccessFor(recipientIdList), message, null); + } + private Pair> sendMessage( SignalServiceDataMessage.Builder messageBuilder, Set recipientIds ) throws IOException { @@ -1491,9 +1664,10 @@ public class Manager implements Closeable { final var addresses = recipientIdList.stream() .map(this::resolveSignalServiceAddress) .collect(Collectors.toList()); - var result = messageSender.sendMessage(addresses, + var result = messageSender.sendDataMessage(addresses, unidentifiedAccessHelper.getAccessFor(recipientIdList), isRecipientUpdate, + ContentHint.DEFAULT, message); for (var r : result) { @@ -1568,7 +1742,7 @@ public class Manager implements Closeable { try { var startTime = System.currentTimeMillis(); - messageSender.sendMessage(syncMessage, unidentifiedAccess); + messageSender.sendSyncMessage(syncMessage, unidentifiedAccess); return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false, @@ -1586,11 +1760,15 @@ public class Manager implements Closeable { final var address = resolveSignalServiceAddress(recipientId); try { try { - return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + return messageSender.sendDataMessage(address, + unidentifiedAccessHelper.getAccessFor(recipientId), + ContentHint.DEFAULT, + message); } catch (UnregisteredUserException e) { final var newRecipientId = refreshRegisteredUser(recipientId); - return messageSender.sendMessage(resolveSignalServiceAddress(newRecipientId), + return messageSender.sendDataMessage(resolveSignalServiceAddress(newRecipientId), unidentifiedAccessHelper.getAccessFor(newRecipientId), + ContentHint.DEFAULT, message); } } catch (UntrustedIdentityException e) { @@ -1774,6 +1952,7 @@ public class Manager implements Closeable { sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); account.getStickerStore().updateSticker(sticker); } + enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); } return actions; } @@ -1849,6 +2028,9 @@ public class Manager implements Closeable { try { action.execute(this); } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } logger.warn("Message action failed.", e); } } @@ -1895,7 +2077,7 @@ public class Manager implements Closeable { boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler - ) throws IOException { + ) throws IOException, InterruptedException { retryFailedReceivedMessages(handler, ignoreAttachments); Set queuedActions = null; @@ -1904,11 +2086,13 @@ public class Manager implements Closeable { var hasCaughtUpWithOldMessages = false; - while (true) { + while (!Thread.interrupted()) { SignalServiceEnvelope envelope; SignalServiceContent content = null; Exception exception = null; final CachedMessage[] cachedMessage = {null}; + account.setLastReceiveTimestamp(System.currentTimeMillis()); + logger.debug("Checking for new message from server"); try { var result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { final var recipientId = envelope1.hasSource() @@ -1917,6 +2101,7 @@ public class Manager implements Closeable { // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); }); + logger.debug("New message received from server"); if (result.isPresent()) { envelope = result.get(); } else { @@ -1928,6 +2113,9 @@ public class Manager implements Closeable { try { action.execute(this); } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } logger.warn("Message action failed.", e); } } @@ -1938,6 +2126,12 @@ public class Manager implements Closeable { // Continue to wait another timeout for new messages continue; } + } catch (AssertionError e) { + if (e.getCause() instanceof InterruptedException) { + throw (InterruptedException) e.getCause(); + } else { + throw e; + } } catch (TimeoutException e) { if (returnOnTimeout) return; continue; @@ -1971,6 +2165,9 @@ public class Manager implements Closeable { try { action.execute(this); } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } logger.warn("Message action failed.", e); } } @@ -2136,7 +2333,16 @@ public class Manager implements Closeable { try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) { var s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; - while ((g = s.read()) != null) { + while (true) { + try { + g = s.read(); + } catch (IOException e) { + logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); + continue; + } + if (g == null) { + break; + } var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); if (syncGroup != null) { if (g.getName().isPresent()) { @@ -2207,7 +2413,17 @@ public class Manager implements Closeable { .asPointer(), tmpFile)) { var s = new DeviceContactsInputStream(attachmentAsStream); DeviceContact c; - while ((c = s.read()) != null) { + while (true) { + try { + c = s.read(); + } catch (IOException e) { + logger.warn("Sync contacts contained invalid contact, ignoring: {}", + e.getMessage()); + continue; + } + if (c == null) { + break; + } if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { account.setProfileKey(c.getProfileKey().get()); } @@ -2274,16 +2490,23 @@ public class Manager implements Closeable { continue; } final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); + final var installed = !m.getType().isPresent() + || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; + var sticker = account.getStickerStore().getSticker(stickerPackId); - if (sticker == null) { - if (!m.getPackKey().isPresent()) { - continue; + if (m.getPackKey().isPresent()) { + if (sticker == null) { + sticker = new Sticker(stickerPackId, m.getPackKey().get()); + } + if (installed) { + enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); } - sticker = new Sticker(stickerPackId, m.getPackKey().get()); } - sticker.setInstalled(!m.getType().isPresent() - || m.getType().get() == StickerPackOperationMessage.Type.INSTALL); - account.getStickerStore().updateSticker(sticker); + + if (sticker != null) { + sticker.setInstalled(installed); + account.getStickerStore().updateSticker(sticker); + } } } if (syncMessage.getFetchType().isPresent()) { @@ -2341,6 +2564,9 @@ public class Manager implements Closeable { avatarStore.storeProfileAvatar(address, outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage()); } } @@ -2618,8 +2844,12 @@ public class Manager implements Closeable { } public GroupInfo getGroup(GroupId groupId) { + return getGroup(groupId, false); + } + + public GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { final var group = account.getGroupStore().getGroup(groupId); - if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { + if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient); account.getGroupStore().updateGroup(group); @@ -2748,6 +2978,11 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveRecipientTrusted(address); } + private void enqueueJob(Job job) { + var context = new Context(account, accountManager, messageReceiver, stickerPackStore); + job.run(context); + } + @Override public void close() throws IOException { close(true);