X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/467a48bac508b56f84dce7ee0b81a22fd0d32161..7f64a9812ca5bb10e8f57cacf3d22b904bd200b4:/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 92dcf4d6..13696235 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -31,9 +31,11 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; @@ -45,7 +47,6 @@ 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; -import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.CachedMessage; @@ -60,23 +61,9 @@ import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; -import org.signal.libsignal.metadata.InvalidMetadataMessageException; -import org.signal.libsignal.metadata.InvalidMetadataVersionException; -import org.signal.libsignal.metadata.ProtocolDuplicateMessageException; -import org.signal.libsignal.metadata.ProtocolInvalidKeyException; -import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; -import org.signal.libsignal.metadata.ProtocolInvalidVersionException; -import org.signal.libsignal.metadata.ProtocolLegacyMessageException; -import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; -import org.signal.libsignal.metadata.SelfSendException; -import org.signal.storageservice.protos.groups.GroupChange; -import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.VerificationFailedException; -import org.signal.zkgroup.groups.GroupMasterKey; -import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.slf4j.Logger; @@ -93,11 +80,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.InvalidMessageStructureException; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -107,7 +92,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.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; @@ -125,18 +109,15 @@ 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.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; -import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; @@ -187,9 +168,9 @@ public class Manager implements Closeable { private final ExecutorService executor = Executors.newCachedThreadPool(); private final ProfileHelper profileHelper; - private final GroupV2Helper groupV2Helper; private final PinHelper pinHelper; private final SendHelper sendHelper; + private final GroupHelper groupHelper; private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; @@ -235,12 +216,11 @@ public class Manager implements Closeable { dependencies::getProfileService, dependencies::getMessageReceiver, this::resolveSignalServiceAddress); - this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, + final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfRecipientId, dependencies.getGroupsV2Operations(), dependencies.getGroupsV2Api(), - this::getGroupAuthForToday, this::resolveSignalServiceAddress); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); @@ -253,6 +233,13 @@ public class Manager implements Closeable { this::handleIdentityFailure, this::getGroup, this::refreshRegisteredUser); + this.groupHelper = new GroupHelper(account, + dependencies, + sendHelper, + groupV2Helper, + avatarStore, + this::resolveSignalServiceAddress, + this::resolveRecipient); } public String getUsername() { @@ -317,7 +304,7 @@ 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."); + 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); @@ -665,15 +652,6 @@ public class Manager implements Closeable { return ProfileUtils.decryptProfile(profileKey, encryptedProfile); } - private Optional createGroupAvatarAttachment(GroupId groupId) throws IOException { - final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); - if (streamDetails == null) { - return Optional.absent(); - } - - return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); - } - private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { final var streamDetails = avatarStore.retrieveContactAvatar(address); if (streamDetails == null) { @@ -683,17 +661,6 @@ public class Manager implements Closeable { return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); } - private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - var g = getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } - if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) { - throw new NotAGroupMemberException(groupId, g.getTitle()); - } - return g; - } - public List getGroups() { return account.getGroupStore().getGroups(); } @@ -701,53 +668,8 @@ public class Manager implements Closeable { public SendGroupMessageResults sendQuitGroupMessage( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - var group = getGroupForUpdating(groupId); - if (group instanceof GroupInfoV1) { - return quitGroupV1((GroupInfoV1) group); - } - final var newAdmins = getRecipientIds(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); - } - } - - private SendGroupMessageResults 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 sendGroupMessage(messageBuilder, - groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - - private SendGroupMessageResults 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); - account.getGroupStore().updateGroup(groupInfoV2); - - var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); - return sendGroupMessage(messageBuilder, - groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + return groupHelper.quitGroup(groupId, newAdmins); } public void deleteGroup(GroupId groupId) throws IOException { @@ -758,45 +680,7 @@ public class Manager implements Closeable { public Pair createGroup( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { - return createGroupInternal(name, members == null ? null : getRecipientIds(members), avatarFile); - } - - private Pair createGroupInternal( - String name, Set members, File avatarFile - ) throws IOException, AttachmentInvalidException { - final var selfRecipientId = account.getSelfRecipientId(); - if (members != null && members.contains(selfRecipientId)) { - members = new HashSet<>(members); - members.remove(selfRecipientId); - } - - var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name, - members == null ? Set.of() : members, - avatarFile); - - if (gv2Pair == null) { - // Failed to create v2 group, creating v1 group instead - var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); - gv1.addMembers(List.of(selfRecipientId)); - final var result = updateGroupV1(gv1, name, members, avatarFile); - return new Pair<>(gv1.getGroupId(), result); - } - - final var gv2 = gv2Pair.first(); - final var decryptedGroup = gv2Pair.second(); - - gv2.setGroup(decryptedGroup, this::resolveRecipient); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(gv2.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - - account.getGroupStore().updateGroup(gv2); - - final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); - - final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); - return new Pair<>(gv2.getGroupId(), result); + return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); } public SendGroupMessageResults updateGroup( @@ -814,8 +698,8 @@ public class Manager implements Closeable { File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - return updateGroupInternal(groupId, + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { + return groupHelper.updateGroup(groupId, name, description, members == null ? null : getRecipientIds(members), @@ -831,272 +715,15 @@ public class Manager implements Closeable { isAnnouncementGroup); } - private SendGroupMessageResults updateGroupInternal( - final GroupId groupId, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - var group = getGroupForUpdating(groupId); - - if (group instanceof GroupInfoV2) { - try { - return updateGroupV2((GroupInfoV2) group, - name, - description, - members, - removeMembers, - admins, - removeAdmins, - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); - } 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, - isAnnouncementGroup); - } - } - - final var gv1 = (GroupInfoV1) group; - final var result = updateGroupV1(gv1, name, members, avatarFile); - if (expirationTimer != null) { - setExpirationTimer(gv1, expirationTimer); - } - return result; - } - - private SendGroupMessageResults updateGroupV1( - final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile - ) throws IOException, AttachmentInvalidException { - updateGroupV1Details(gv1, name, members, avatarFile); - - account.getGroupStore().updateGroup(gv1); - - var messageBuilder = getGroupUpdateMessageBuilder(gv1); - return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - - private void updateGroupV1Details( - final GroupInfoV1 g, final String name, final Collection members, final File avatarFile - ) throws IOException { - if (name != null) { - g.name = name; - } - - if (members != null) { - final var newMemberAddresses = members.stream() - .filter(member -> !g.isMember(member)) - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()); - final var newE164Members = new HashSet(); - for (var member : newMemberAddresses) { - if (!member.getNumber().isPresent()) { - continue; - } - newE164Members.add(member.getNumber().get()); - } - - final var registeredUsers = getRegisteredUsers(newE164Members); - if (registeredUsers.size() != newE164Members.size()) { - // Some of the new members are not registered on Signal - newE164Members.removeAll(registeredUsers.keySet()); - throw new IOException("Failed to add members " - + String.join(", ", newE164Members) - + " to group: Not registered on Signal"); - } - - g.addMembers(members); - } - - if (avatarFile != null) { - avatarStore.storeGroupAvatar(g.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - } - - private SendGroupMessageResults updateGroupV2( - final GroupInfoV2 group, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup - ) throws IOException { - SendGroupMessageResults result = null; - if (group.isPendingMember(account.getSelfRecipientId())) { - var groupGroupChangePair = groupV2Helper.acceptInvite(group); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (members != null) { - final var newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers()); - if (newMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - } - - if (removeMembers != null) { - var existingRemoveMembers = new HashSet<>(removeMembers); - existingRemoveMembers.retainAll(group.getMembers()); - existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage - if (existingRemoveMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - var pendingRemoveMembers = new HashSet<>(removeMembers); - pendingRemoveMembers.retainAll(group.getPendingMembers()); - if (pendingRemoveMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - } - - if (admins != null) { - final var newAdmins = new HashSet<>(admins); - newAdmins.retainAll(group.getMembers()); - newAdmins.removeAll(group.getAdminMembers()); - if (newAdmins.size() > 0) { - for (var admin : newAdmins) { - var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); - result = sendUpdateGroupV2Message(group, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - } - - if (removeAdmins != null) { - final var existingRemoveAdmins = new HashSet<>(removeAdmins); - existingRemoveAdmins.retainAll(group.getAdminMembers()); - if (existingRemoveAdmins.size() > 0) { - for (var admin : existingRemoveAdmins) { - var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); - result = sendUpdateGroupV2Message(group, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - } - - 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 (isAnnouncementGroup != null) { - var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup); - 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(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - return result; - } - public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { - final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), - inviteLinkUrl.getPassword()); - final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(), - inviteLinkUrl.getPassword(), - groupJoinInfo); - final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), - groupJoinInfo.getRevision() + 1, - groupChange.toByteArray()); - - if (group.getGroup() == null) { - // Only requested member, can't send update to group members - return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of())); - } - - final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); - - return new Pair<>(group.getGroupId(), result); - } - - private SendGroupMessageResults sendUpdateGroupV2Message( - GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange - ) throws IOException { - final var selfRecipientId = account.getSelfRecipientId(); - final var members = group.getMembersIncludingPendingWithout(selfRecipientId); - group.setGroup(newDecryptedGroup, this::resolveRecipient); - members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId)); - account.getGroupStore().updateGroup(group); - - final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - return sendGroupMessage(messageBuilder, members); + return groupHelper.joinGroup(inviteLinkUrl); } public SendMessageResults sendMessage( SignalServiceDataMessage.Builder messageBuilder, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var results = new HashMap>(); long timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); @@ -1119,7 +746,7 @@ public class Manager implements Closeable { public void sendTypingMessage( SignalServiceTypingMessage.Action action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var timestamp = System.currentTimeMillis(); for (var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single) { @@ -1134,100 +761,18 @@ public class Manager implements Closeable { } } - private SendGroupMessageResults sendGroupMessage( - final SignalServiceDataMessage.Builder messageBuilder, final Set members - ) throws IOException { - final var timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members); - return new SendGroupMessageResults(timestamp, results); - } - - private static int currentTimeDays() { - return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); - } - - private GroupsV2AuthorizationString getGroupAuthForToday( - final GroupSecretParams groupSecretParams - ) throws IOException { - final var today = currentTimeDays(); - // Returns credentials for the next 7 days - final var credentials = dependencies.getGroupsV2Api().getCredentials(today); - // TODO cache credentials until they expire - var authCredentialResponse = credentials.get(today); - try { - return dependencies.getGroupsV2Api() - .getGroupsV2AuthorizationString(account.getUuid(), - today, - groupSecretParams, - authCredentialResponse); - } catch (VerificationFailedException e) { - throw new IOException(e); - } - } - SendGroupMessageResults sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { - GroupInfoV1 g; - var group = getGroupForUpdating(groupId); - if (!(group instanceof GroupInfoV1)) { - throw new IOException("Received an invalid group request for a v2 group!"); - } - g = (GroupInfoV1) group; - final var recipientId = resolveRecipient(recipient); - if (!g.isMember(recipientId)) { - throw new NotAGroupMemberException(groupId, g.name); - } - - var messageBuilder = getGroupUpdateMessageBuilder(g); - - // Send group message only to the recipient who requested it - return sendGroupMessage(messageBuilder, Set.of(recipientId)); - } - - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) - .withId(g.getGroupId().serialize()) - .withName(g.name) - .withMembers(g.getMembers() - .stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList())); - - try { - final var attachment = createGroupAvatarAttachment(g.getGroupId()); - if (attachment.isPresent()) { - group.withAvatar(attachment.get()); - } - } catch (IOException e) { - throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); - } - - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); - } - - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { - var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) - .withRevision(g.getGroup().getRevision()) - .withSignedGroupChange(signedGroupChange); - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return groupHelper.sendGroupInfoMessage(groupId, recipientId); } SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); - - // Send group info request message to the recipient who sent us a message with this groupId - return sendGroupMessage(messageBuilder, Set.of(resolveRecipient(recipient))); + final var recipientId = resolveRecipient(recipient); + return groupHelper.sendGroupInfoRequest(groupId, recipientId); } public void sendReadReceipt( @@ -1262,7 +807,7 @@ public class Manager implements Closeable { public SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); @@ -1292,7 +837,7 @@ public class Manager implements Closeable { public SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); return sendMessage(messageBuilder, recipients); @@ -1304,7 +849,7 @@ public class Manager implements Closeable { RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var targetAuthorRecipientId = resolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, @@ -1320,7 +865,7 @@ public class Manager implements Closeable { try { return sendMessage(messageBuilder, recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } finally { for (var recipient : recipients) { @@ -1361,6 +906,7 @@ public class Manager implements Closeable { private void setContactBlocked(RecipientId recipientId, boolean blocked) { var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + // TODO cycle our profile key account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); } @@ -1371,19 +917,10 @@ public class Manager implements Closeable { } group.setBlocked(blocked); + // TODO cycle our profile key account.getGroupStore().updateGroup(group); } - private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { - var contact = account.getContactStore().getContact(recipientId); - if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { - return; - } - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore() - .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); - } - /** * Change the expiration timer for a contact */ @@ -1395,25 +932,19 @@ public class Manager implements Closeable { final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { sendMessage(messageBuilder, Set.of(recipient)); - } catch (NotAGroupMemberException | GroupNotFoundException e) { + } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } } - /** - * Change the expiration timer for a group - */ - 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(); - sendHelper.sendAsGroupMessage(messageBuilder, groupId); + private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { + var contact = account.getContactStore().getContact(recipientId); + if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { + return; + } + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore() + .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); } /** @@ -1579,14 +1110,10 @@ public class Manager implements Closeable { public void sendTypingMessage( TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { sendTypingMessage(action.toSignalService(), recipients); } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { - return dependencies.getCipher().decrypt(envelope); - } - private void handleEndSession(RecipientId recipientId) { account.getSessionStore().deleteAllSessions(recipientId); } @@ -1614,7 +1141,7 @@ public class Manager implements Closeable { if (groupInfo.getAvatar().isPresent()) { var avatar = groupInfo.getAvatar().get(); - downloadGroupAvatar(avatar, groupV1.getGroupId()); + downloadGroupAvatar(groupV1.getGroupId(), avatar); } if (groupInfo.getName().isPresent()) { @@ -1658,7 +1185,7 @@ public class Manager implements Closeable { final var groupContext = message.getGroupContext().get().getGroupV2().get(); final var groupMasterKey = groupContext.getMasterKey(); - getOrMigrateGroup(groupMasterKey, + groupHelper.getOrMigrateGroup(groupMasterKey, groupContext.getRevision(), groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); } @@ -1743,65 +1270,6 @@ public class Manager implements Closeable { return actions; } - private GroupInfoV2 getOrMigrateGroup( - final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange - ) { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - - var groupId = GroupUtils.getGroupIdV2(groupSecretParams); - var groupInfo = getGroup(groupId); - final GroupInfoV2 groupInfoV2; - if (groupInfo instanceof GroupInfoV1) { - // Received a v2 group message for a v1 group, we need to locally migrate the group - account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId()); - groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - logger.info("Locally migrated group {} to group v2, id: {}", - groupInfo.getGroupId().toBase64(), - groupInfoV2.getGroupId().toBase64()); - } else if (groupInfo instanceof GroupInfoV2) { - groupInfoV2 = (GroupInfoV2) groupInfo; - } else { - groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - } - - if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) { - DecryptedGroup group = null; - if (signedGroupChange != null - && groupInfoV2.getGroup() != null - && groupInfoV2.getGroup().getRevision() + 1 == revision) { - group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), - signedGroupChange, - groupMasterKey); - } - if (group == null) { - group = groupV2Helper.getDecryptedGroup(groupSecretParams); - } - if (group != null) { - storeProfileKeysFromMembers(group); - final var avatar = group.getAvatar(); - if (avatar != null && !avatar.isEmpty()) { - downloadGroupAvatar(groupId, groupSecretParams, avatar); - } - } - groupInfoV2.setGroup(group, this::resolveRecipient); - account.getGroupStore().updateGroup(groupInfoV2); - } - - return groupInfoV2; - } - - private void storeProfileKeysFromMembers(final DecryptedGroup group) { - for (var member : group.getMembersList()) { - final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray()); - final var recipientId = account.getRecipientStore().resolveRecipient(uuid); - try { - account.getProfileStore() - .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); - } catch (InvalidInputException ignored) { - } - } - } - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { @@ -1824,7 +1292,7 @@ public class Manager implements Closeable { List actions = null; if (!envelope.isReceipt()) { try { - content = decryptMessage(envelope); + content = dependencies.getCipher().decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { if (!envelope.hasSource()) { final var identifier = e.getSender(); @@ -1915,7 +1383,7 @@ public class Manager implements Closeable { } if (!envelope.isReceipt()) { try { - content = decryptMessage(envelope); + content = dependencies.getCipher().decrypt(envelope); } catch (Exception e) { exception = e; } @@ -2063,9 +1531,20 @@ public class Manager implements Closeable { } final var recipientId = resolveRecipient(source); - return !group.isMember(recipientId) || ( - group.isAnnouncementGroup() && !group.isAdmin(recipientId) - ); + if (!group.isMember(recipientId)) { + return true; + } + + if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { + return message.getBody().isPresent() + || message.getAttachments().isPresent() + || message.getQuote() + .isPresent() + || message.getPreviews().isPresent() + || message.getMentions().isPresent() + || message.getSticker().isPresent(); + } + return false; } private List handleMessage( @@ -2157,7 +1636,7 @@ public class Manager implements Closeable { } if (g.getAvatar().isPresent()) { - downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId()); + downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); } syncGroup.archived = g.isArchived(); account.getGroupStore().updateGroup(syncGroup); @@ -2333,7 +1812,7 @@ public class Manager implements Closeable { } } - private void downloadGroupAvatar(SignalServiceAttachment avatar, GroupId groupId) { + private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { try { avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream)); } catch (IOException e) { @@ -2341,15 +1820,6 @@ public class Manager implements Closeable { } } - private void downloadGroupAvatar(GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey) { - try { - avatarStore.storeGroupAvatar(groupId, - outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); - } - } - private void downloadProfileAvatar( SignalServiceAddress address, String avatarPath, ProfileKey profileKey ) { @@ -2392,29 +1862,6 @@ public class Manager implements Closeable { } } - private void retrieveGroupV2Avatar( - GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream - ) throws IOException { - var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); - - var tmpFile = IOUtils.createTempFile(); - try (InputStream input = dependencies.getMessageReceiver() - .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { - var encryptedData = IOUtils.readFully(input); - - var decryptedData = groupOperations.decryptAvatar(encryptedData); - outputStream.write(decryptedData); - } finally { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - private void retrieveProfileAvatar( String avatarPath, ProfileKey profileKey, OutputStream outputStream ) throws IOException { @@ -2490,7 +1937,7 @@ public class Manager implements Closeable { .stream() .map(this::resolveSignalServiceAddress) .collect(Collectors.toList()), - createGroupAvatarAttachment(groupInfo.getGroupId()), + groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), groupInfo.isMember(account.getSelfRecipientId()), Optional.of(groupInfo.messageExpirationTime), Optional.fromNullable(groupInfo.color), @@ -2639,17 +2086,7 @@ 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 && (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); - } - return group; + return groupHelper.getGroup(groupId); } public List getIdentities() {