diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 09bea00ad8..91715cc92b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -132,6 +132,21 @@ public final class GroupDatabase extends Database { return Optional.fromNullable(reader.getCurrent()); } + /** + * Call if you are sure this group should exist. + *

+ * Finds group and throws if it cannot. + */ + public @NonNull GroupRecord requireGroup(@NonNull GroupId groupId) { + Optional group = getGroup(groupId); + + if (!group.isPresent()) { + throw new AssertionError("Group not found"); + } + + return group.get(); + } + public boolean isUnknownGroup(@NonNull GroupId groupId) { Optional group = getGroup(groupId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 66c53931f2..ec37bc2b8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; +import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; @@ -791,7 +792,7 @@ public class MmsDatabase extends MessagingDatabase { } if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) { - return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, false, quote, contacts, previews); + return new OutgoingGroupMediaMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews); } else if (Types.isExpirationTimerUpdate(outboxType)) { return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn); } @@ -1050,8 +1051,16 @@ public class MmsDatabase extends MessagingDatabase { if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; if (message.isGroup()) { - if (((OutgoingGroupMediaMessage)message).isGroupUpdate()) type |= Types.GROUP_UPDATE_BIT; - else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT; + OutgoingGroupMediaMessage outgoingGroupMediaMessage = (OutgoingGroupMediaMessage) message; + if (outgoingGroupMediaMessage.isV2Group()) { + MessageGroupContext.GroupV2Properties groupV2Properties = outgoingGroupMediaMessage.requireGroupV2Properties(); + type |= Types.GROUP_V2_BIT; + if (groupV2Properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT; + } else { + MessageGroupContext.GroupV1Properties properties = outgoingGroupMediaMessage.requireGroupV1Properties(); + if (properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT; + else if (properties.isQuit()) type |= Types.GROUP_QUIT_BIT; + } } if (message.isExpirationUpdate()) { @@ -1090,11 +1099,24 @@ public class MmsDatabase extends MessagingDatabase { long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener); if (message.getRecipient().isGroup()) { - GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); + OutgoingGroupMediaMessage outgoingGroupMediaMessage = (message instanceof OutgoingGroupMediaMessage) ? (OutgoingGroupMediaMessage) message : null; - receiptDatabase.insert(Stream.of(members).map(Recipient::getId).toList(), - messageId, defaultReceiptStatus, message.getSentTimeMillis()); + GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); + RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); + Set members = new HashSet<>(); + + if (outgoingGroupMediaMessage != null && outgoingGroupMediaMessage.isV2Group()) { + MessageGroupContext.GroupV2Properties groupV2Properties = outgoingGroupMediaMessage.requireGroupV2Properties(); + members.addAll(Stream.of(groupV2Properties.getActiveMembers()).map(recipientDatabase::getOrInsertFromUuid).toList()); + if (groupV2Properties.isUpdate()) { + members.addAll(Stream.of(groupV2Properties.getPendingMembers()).map(recipientDatabase::getOrInsertFromUuid).toList()); + } + members.remove(Recipient.self().getId()); + } else { + members.addAll(Stream.of(DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)).map(Recipient::getId).toList()); + } + + receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis()); for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_DELIVERED, -1); for (RecipientId recipientId : earlyReadReceipts.keySet()) receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_READ, -1); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index ca01455fa7..52bea99579 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -76,6 +76,7 @@ public interface MmsSmsColumns { protected static final long GROUP_UPDATE_BIT = 0x10000; protected static final long GROUP_QUIT_BIT = 0x20000; protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000; + protected static final long GROUP_V2_BIT = 0x80000; // Encrypted Storage Information XXX public static final long ENCRYPTION_MASK = 0xFF000000; @@ -223,6 +224,10 @@ public interface MmsSmsColumns { return (type & GROUP_UPDATE_BIT) != 0; } + public static boolean isGroupV2(long type) { + return (type & GROUP_V2_BIT) != 0; + } + public static boolean isGroupQuit(long type) { return (type & GROUP_QUIT_BIT) != 0; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index f95cdb331f..e9d82523db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; +import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.Attachment; @@ -26,6 +27,7 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; @@ -44,15 +46,18 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Quote; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; public class PushGroupSendJob extends PushSendJob { @@ -147,24 +152,25 @@ public class PushGroupSendJob extends PushSendJob { return; } - if (!message.getRecipient().isPushGroup()) { + Recipient groupRecipient = message.getRecipient().fresh(); + + if (!groupRecipient.isPushGroup()) { throw new MmsException("Message recipient isn't a group!"); } try { log(TAG, "Sending message: " + messageId); - if (!message.getRecipient().resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) { - RecipientUtil.shareProfileIfFirstSecureMessage(context, message.getRecipient()); + if (!groupRecipient.resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) { + RecipientUtil.shareProfileIfFirstSecureMessage(context, groupRecipient); } List target; - Recipient groupRecipient = message.getRecipient().fresh(); - - if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient).getId()); - else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).toList(); - else target = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId); + if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient).getId()); + else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).toList(); + else if (groupRecipient.isPushV2Group() && message instanceof OutgoingGroupMediaMessage) target = getGroupMessageV2Recipients((OutgoingGroupMediaMessage) message); + else target = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId); List results = deliver(message, groupRecipient, target); List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Recipient.externalPush(context, result.getAddress()).getId())).toList(); @@ -260,37 +266,72 @@ public class PushGroupSendJob extends PushSendJob { .toList(); if (message.isGroup()) { - OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; - GroupContext groupContext = groupMessage.getGroupContext(); - SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0); - SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE; - List members = Stream.of(groupContext.getMembersList()) - .map(m -> new SignalServiceAddress(UuidUtil.parseOrNull(m.getUuid()), m.getE164())) - .toList(); - SignalServiceGroup group = new SignalServiceGroup(type, groupId.getDecodedId(), groupContext.getName(), members, avatar); - SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() + OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; + + if (groupMessage.isV2Group()) { + MessageGroupContext.GroupV2Properties properties = groupMessage.requireGroupV2Properties(); + GroupContextV2 groupContext = properties.getGroupContext(); + SignalServiceGroupV2.Builder builder = SignalServiceGroupV2.newBuilder(properties.getGroupMasterKey()) + .withRevision(groupContext.getRevision()); + + ByteString groupChange = groupContext.getGroupChange(); + if (groupChange != null) { + builder.withSignedGroupChange(groupChange.toByteArray()); + } + + SignalServiceGroupV2 group = builder.build(); + SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getSentTimeMillis()) .withExpiration(groupRecipient.getExpireMessages()) .asGroupMessage(group) .build(); - return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage); + return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage); + } else { + MessageGroupContext.GroupV1Properties properties = groupMessage.requireGroupV1Properties(); + + GroupContext groupContext = properties.getGroupContext(); + SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0); + SignalServiceGroup.Type type = properties.isQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE; + List members = Stream.of(groupContext.getMembersList()) + .map(m -> new SignalServiceAddress(UuidUtil.parseOrNull(m.getUuid()), m.getE164())) + .toList(); + SignalServiceGroup group = new SignalServiceGroup(type, groupId.getDecodedId(), groupContext.getName(), members, avatar); + SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() + .withTimestamp(message.getSentTimeMillis()) + .withExpiration(message.getRecipient().getExpireMessages()) + .asGroupMessage(group) + .build(); + + return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage); + } } else { - SignalServiceGroup group = new SignalServiceGroup(groupId.getDecodedId()); - SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getSentTimeMillis()) - .asGroupMessage(group) - .withAttachments(attachmentPointers) - .withBody(message.getBody()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .withViewOnce(message.isViewOnce()) - .asExpirationUpdate(message.isExpirationUpdate()) - .withProfileKey(profileKey.orNull()) - .withQuote(quote.orNull()) - .withSticker(sticker.orNull()) - .withSharedContacts(sharedContacts) - .withPreviews(previews) - .build(); + SignalServiceDataMessage.Builder builder = SignalServiceDataMessage.newBuilder() + .withTimestamp(message.getSentTimeMillis()); + + if (groupId.isV2()) { + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId); + GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties(); + SignalServiceGroupV2 group = SignalServiceGroupV2.newBuilder(v2GroupProperties.getGroupMasterKey()) + .withRevision(v2GroupProperties.getGroupRevision()) + .build(); + builder.asGroupMessage(group); + } else { + builder.asGroupMessage(new SignalServiceGroup(groupId.getDecodedId())); + } + + SignalServiceDataMessage groupMessage = builder.withAttachments(attachmentPointers) + .withBody(message.getBody()) + .withExpiration((int)(message.getExpiresIn() / 1000)) + .withViewOnce(message.isViewOnce()) + .asExpirationUpdate(message.isExpirationUpdate()) + .withProfileKey(profileKey.orNull()) + .withQuote(quote.orNull()) + .withSticker(sticker.orNull()) + .withSharedContacts(sharedContacts) + .withPreviews(previews) + .build(); return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupMessage); } @@ -298,12 +339,27 @@ public class PushGroupSendJob extends PushSendJob { private @NonNull List getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) { List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId); + if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList(); List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); return Stream.of(members).map(Recipient::getId).toList(); } + private @NonNull List getGroupMessageV2Recipients(@NonNull OutgoingGroupMediaMessage message) { + UUID selfUUId = Recipient.self().getUuid().get(); + MessageGroupContext.GroupV2Properties groupV2Properties = message.requireGroupV2Properties(); + boolean includePending = groupV2Properties.isUpdate(); + + return Stream.concat(Stream.of(groupV2Properties.getActiveMembers()), + includePending ? Stream.of(groupV2Properties.getPendingMembers()) : Stream.empty()) + .filterNot(selfUUId::equals) + .distinct() + .map(uuid -> Recipient.externalPush(context, uuid, null)) + .map(Recipient::getId) + .toList(); + } + public static class Factory implements Job.Factory { @Override public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java b/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java new file mode 100644 index 0000000000..a2cd22cde2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java @@ -0,0 +1,134 @@ +package org.thoughtcrime.securesms.mms; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Stream; + +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.signal.zkgroup.util.UUIDUtil; +import org.thoughtcrime.securesms.util.Base64; +import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2; +import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +/** + * Represents either a GroupV1 or GroupV2 encoded context. + */ +public final class MessageGroupContext { + + private final String encodedGroupContext; + private final GroupV1Properties groupV1; + private final GroupV2Properties groupV2; + + public MessageGroupContext(@NonNull String encodedGroupContext, boolean v2) + throws IOException + { + this.encodedGroupContext = encodedGroupContext; + if (v2) { + this.groupV1 = null; + this.groupV2 = new GroupV2Properties(DecryptedGroupV2Context.parseFrom(Base64.decode(encodedGroupContext))); + } else { + this.groupV1 = new GroupV1Properties(GroupContext.parseFrom(Base64.decode(encodedGroupContext))); + this.groupV2 = null; + } + } + + public MessageGroupContext(@NonNull GroupContext group) { + this.encodedGroupContext = Base64.encodeBytes(group.toByteArray()); + this.groupV1 = new GroupV1Properties(group); + this.groupV2 = null; + } + + public MessageGroupContext(@NonNull DecryptedGroupV2Context group) { + this.encodedGroupContext = Base64.encodeBytes(group.toByteArray()); + this.groupV1 = null; + this.groupV2 = new GroupV2Properties(group); + } + + public @NonNull GroupV1Properties requireGroupV1Properties() { + if (groupV1 == null) { + throw new AssertionError(); + } + return groupV1; + } + + public @NonNull GroupV2Properties requireGroupV2Properties() { + if (groupV2 == null) { + throw new AssertionError(); + } + return groupV2; + } + + public boolean isV2Group() { + return groupV2 != null; + } + + public @NonNull String getEncodedGroupContext() { + return encodedGroupContext; + } + + public static class GroupV1Properties { + + private final GroupContext groupContext; + + private GroupV1Properties(GroupContext groupContext) { + this.groupContext = groupContext; + } + + public @NonNull GroupContext getGroupContext() { + return groupContext; + } + + public boolean isQuit() { + return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE; + } + + public boolean isUpdate() { + return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE; + } + } + + public static class GroupV2Properties { + + private final DecryptedGroupV2Context decryptedGroupV2Context; + private final GroupContextV2 groupContext; + private final GroupMasterKey groupMasterKey; + + private GroupV2Properties(DecryptedGroupV2Context decryptedGroupV2Context) { + this.decryptedGroupV2Context = decryptedGroupV2Context; + this.groupContext = decryptedGroupV2Context.getContext(); + try { + groupMasterKey = new GroupMasterKey(groupContext.getMasterKey().toByteArray()); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + } + + public @NonNull GroupContextV2 getGroupContext() { + return groupContext; + } + + public @NonNull GroupMasterKey getGroupMasterKey() { + return groupMasterKey; + } + + public @NonNull List getActiveMembers() { + return DecryptedGroupUtil.membersToUuidList(decryptedGroupV2Context.getGroupState().getMembersList()); + } + + public @NonNull List getPendingMembers() { + return DecryptedGroupUtil.pendingToUuidList(decryptedGroupV2Context.getGroupState().getPendingMembersList()); + } + + public boolean isUpdate() { + // The group context is only stored on update messages. + return true; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java index 2fb734f87c..3ff807b157 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java @@ -6,21 +6,20 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; -import java.io.IOException; -import java.util.LinkedList; +import java.util.Collections; import java.util.List; -public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { +public final class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { - private final GroupContext group; + private final MessageGroupContext messageGroupContext; public OutgoingGroupMediaMessage(@NonNull Recipient recipient, - @NonNull String encodedGroupContext, + @NonNull MessageGroupContext groupContext, @NonNull List avatar, long sentTimeMillis, long expiresIn, @@ -28,12 +27,11 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { @Nullable QuoteModel quote, @NonNull List contacts, @NonNull List previews) - throws IOException { - super(recipient, encodedGroupContext, avatar, sentTimeMillis, + super(recipient, groupContext.getEncodedGroupContext(), avatar, sentTimeMillis, ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, viewOnce, quote, contacts, previews); - this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext)); + this.messageGroupContext = groupContext; } public OutgoingGroupMediaMessage(@NonNull Recipient recipient, @@ -46,12 +44,20 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { @NonNull List contacts, @NonNull List previews) { - super(recipient, Base64.encodeBytes(group.toByteArray()), - new LinkedList() {{if (avatar != null) add(avatar);}}, - System.currentTimeMillis(), - ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, viewOnce, quote, contacts, previews); + this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews); + } - this.group = group; + public OutgoingGroupMediaMessage(@NonNull Recipient recipient, + @NonNull DecryptedGroupV2Context group, + @Nullable final Attachment avatar, + long sentTimeMillis, + long expireIn, + boolean viewOnce, + @Nullable QuoteModel quote, + @NonNull List contacts, + @NonNull List previews) + { + this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews); } @Override @@ -59,15 +65,19 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { return true; } - public boolean isGroupUpdate() { - return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE; + public boolean isV2Group() { + return messageGroupContext.isV2Group(); } - public boolean isGroupQuit() { - return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE; + public @NonNull MessageGroupContext.GroupV1Properties requireGroupV1Properties() { + return messageGroupContext.requireGroupV1Properties(); } - public GroupContext getGroupContext() { - return group; + public @NonNull MessageGroupContext.GroupV2Properties requireGroupV2Properties() { + return messageGroupContext.requireGroupV2Properties(); + } + + private static List getAttachments(@Nullable Attachment avatar) { + return avatar == null ? Collections.emptyList() : Collections.singletonList(avatar); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index e09fa8a4f9..df3a4f2914 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -575,6 +575,11 @@ public class Recipient { return groupId != null && groupId.isPush(); } + public boolean isPushV2Group() { + GroupId groupId = resolve().groupId; + return groupId != null && groupId.isV2(); + } + public @NonNull List getParticipants() { return new ArrayList<>(participants); } diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 83fa88aeec..fcac83d00e 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -22,3 +22,13 @@ message ReactionList { repeated Reaction reactions = 1; } + + +import "SignalService.proto"; +import "DecryptedGroups.proto"; + +message DecryptedGroupV2Context { + signalservice.GroupContextV2 context = 1; + DecryptedGroupChange change = 2; + DecryptedGroup groupState = 3; +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java index e67e030d79..526f8250cb 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java @@ -39,6 +39,16 @@ public final class DecryptedGroupUtil { return uuidList; } + public static ArrayList membersToUuidList(Collection membersList) { + ArrayList uuidList = new ArrayList<>(membersList.size()); + + for (DecryptedMember member : membersList) { + uuidList.add(toUuid(member)); + } + + return uuidList; + } + public static ArrayList pendingToUuidList(Collection membersList) { ArrayList uuidList = new ArrayList<>(membersList.size());