GV2 message contexts.

master
Alan Evans 2020-05-13 13:36:57 -03:00 committed by Alex Hart
parent f099c3591c
commit b8df90531f
14 changed files with 286 additions and 108 deletions

View File

@ -103,7 +103,7 @@ public class ConversationUpdateItem extends LinearLayout
}
if (this.messageRecord != null && messageRecord.isGroupAction()) {
GroupUtil.getDescription(getContext(), messageRecord.getBody()).removeObserver(this);
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).removeObserver(this);
}
this.messageRecord = messageRecord;
@ -113,7 +113,7 @@ public class ConversationUpdateItem extends LinearLayout
this.sender.observeForever(this);
if (this.messageRecord != null && messageRecord.isGroupAction()) {
GroupUtil.getDescription(getContext(), messageRecord.getBody()).addObserver(this);
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).addObserver(this);
}
present(messageRecord);
@ -236,7 +236,7 @@ public class ConversationUpdateItem extends LinearLayout
sender.removeForeverObserver(this);
}
if (this.messageRecord != null && messageRecord.isGroupAction()) {
GroupUtil.getDescription(getContext(), messageRecord.getBody()).removeObserver(this);
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).removeObserver(this);
}
}

View File

@ -1137,7 +1137,11 @@ public class MmsDatabase extends MessagingDatabase {
MessageGroupContext.GroupV2Properties groupV2Properties = outgoingGroupUpdateMessage.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.addAll(Stream.concat(Stream.of(groupV2Properties.getPendingMembers()),
Stream.of(groupV2Properties.getRemovedMembers()))
.distinct()
.map(recipientDatabase::getOrInsertFromUuid)
.toList());
}
members.remove(Recipient.self().getId());
} else {

View File

@ -616,9 +616,14 @@ public class SmsDatabase extends MessagingDatabase {
} else if (message.isSecureMessage()) {
type |= Types.SECURE_MESSAGE_BIT;
} else if (message.isGroup()) {
IncomingGroupUpdateMessage incomingGroupUpdateMessage = (IncomingGroupUpdateMessage) message;
type |= Types.SECURE_MESSAGE_BIT;
if (((IncomingGroupUpdateMessage)message).isUpdate()) type |= Types.GROUP_UPDATE_BIT;
else if (((IncomingGroupUpdateMessage)message).isQuit()) type |= Types.GROUP_QUIT_BIT;
if (incomingGroupUpdateMessage.isGroupV2()) type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT;
else if (incomingGroupUpdateMessage.isUpdate()) type |= Types.GROUP_UPDATE_BIT;
else if (incomingGroupUpdateMessage.isQuit()) type |= Types.GROUP_QUIT_BIT;
} else if (message.isEndSession()) {
type |= Types.SECURE_MESSAGE_BIT;
type |= Types.END_SESSION_BIT;

View File

@ -17,9 +17,10 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import androidx.annotation.NonNull;
import android.text.SpannableString;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -111,6 +112,10 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isGroupUpdate(type);
}
public boolean isGroupV2() {
return SmsDatabase.Types.isGroupV2(type);
}
public boolean isGroupQuit() {
return SmsDatabase.Types.isGroupQuit(type);
}

View File

@ -99,7 +99,7 @@ public abstract class MessageRecord extends DisplayRecord {
if (isGroupUpdate() && isOutgoing()) {
return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group));
} else if (isGroupUpdate()) {
return new SpannableString(GroupUtil.getDescription(context, getBody()).toString(getIndividualRecipient()));
return new SpannableString(GroupUtil.getDescription(context, getBody(), false).toString(getIndividualRecipient()));
} else if (isGroupQuit() && isOutgoing()) {
return new SpannableString(context.getString(R.string.MessageRecord_left_group));
} else if (isGroupQuit()) {

View File

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.groups;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
@ -30,8 +29,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
@ -154,7 +151,7 @@ final class GroupManagerV1 {
static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) {
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
Optional<OutgoingGroupUpdateMessage> leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient);
Optional<OutgoingGroupUpdateMessage> leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient);
if (threadId != -1 && leaveMessage.isPresent()) {
try {
@ -180,7 +177,7 @@ final class GroupManagerV1 {
if (DatabaseFactory.getGroupDatabase(context).isActive(groupId)) {
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
Optional<OutgoingGroupUpdateMessage> leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient);
Optional<OutgoingGroupUpdateMessage> leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient);
if (threadId != -1 && leaveMessage.isPresent()) {
ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(groupRecipient));
@ -210,4 +207,32 @@ final class GroupManagerV1 {
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(recipient, System.currentTimeMillis(), expirationTime * 1000L);
MessageSender.send(context, outgoingMessage, threadId, false, null);
}
@WorkerThread
private static Optional<OutgoingGroupUpdateMessage> createGroupLeaveMessage(@NonNull Context context,
@NonNull GroupId.V1 groupId,
@NonNull Recipient groupRecipient)
{
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
if (!groupDatabase.isActive(groupId)) {
Log.w(TAG, "Group has already been left.");
return Optional.absent();
}
GroupContext groupContext = GroupContext.newBuilder()
.setId(ByteString.copyFrom(groupId.getDecodedId()))
.setType(GroupContext.Type.QUIT)
.build();
return Optional.of(new OutgoingGroupUpdateMessage(groupRecipient,
groupContext,
null,
System.currentTimeMillis(),
0,
false,
null,
Collections.emptyList(),
Collections.emptyList()));
}
}

View File

@ -7,13 +7,16 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.util.UUIDUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
@ -29,10 +32,14 @@ import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
import java.io.IOException;
@ -132,7 +139,14 @@ public final class GroupsV2StateProcessor {
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
}
GlobalGroupState inputGroupState = queryServer();
GlobalGroupState inputGroupState;
try {
inputGroupState = queryServer();
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message");
insertGroupLeave();
throw e;
}
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision);
DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState();
@ -152,6 +166,49 @@ public final class GroupsV2StateProcessor {
return new GroupUpdateResult(GroupState.GROUP_UPDATED, newLocalState);
}
private void insertGroupLeave() {
if (!groupDatabase.isActive(groupId)) {
Log.w(TAG, "Group has already been left.");
return;
}
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
UUID selfUuid = Recipient.self().getUuid().get();
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId)
.requireV2GroupProperties()
.getDecryptedGroup();
DecryptedGroup simulatedGroupState = DecryptedGroupUtil.removeMember(decryptedGroup, selfUuid, decryptedGroup.getVersion() + 1);
DecryptedGroupChange simulatedGroupChange = DecryptedGroupChange.newBuilder()
.setEditor(UuidUtil.toByteString(UuidUtil.UNKNOWN_UUID))
.setVersion(simulatedGroupState.getVersion())
.addDeleteMembers(UuidUtil.toByteString(selfUuid))
.build();
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, simulatedGroupState, simulatedGroupChange);
OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage(groupRecipient,
decryptedGroupV2Context,
null,
System.currentTimeMillis(),
0,
false,
null,
Collections.emptyList(),
Collections.emptyList());
try {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
long id = mmsDatabase.insertMessageOutbox(leaveMessage, threadId, false, null);
mmsDatabase.markAsSent(id, true);
} catch (MmsException e) {
Log.w(TAG, "Failed to insert leave message.", e);
}
groupDatabase.setActive(groupId, false);
groupDatabase.remove(groupId, Recipient.self().getId());
}
/**
* @return true iff group exists locally and is at least the specified revision.
*/
@ -252,17 +309,29 @@ public final class GroupsV2StateProcessor {
}
private void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
try {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
RecipientId recipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
Recipient recipient = Recipient.resolved(recipientId);
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(recipient, decryptedGroupV2Context, null, timestamp, 0, false, null, Collections.emptyList(), Collections.emptyList());
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
UUID editor = DecryptedGroupUtil.editorUuid(decryptedGroupV2Context.getChange());
boolean outgoing = Recipient.self().getUuid().get().equals(editor);
mmsDatabase.markAsSent(messageId, true);
} catch (MmsException e) {
Log.w(TAG, e);
if (outgoing) {
try {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
RecipientId recipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
Recipient recipient = Recipient.resolved(recipientId);
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(recipient, decryptedGroupV2Context, null, timestamp, 0, false, null, Collections.emptyList(), Collections.emptyList());
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
mmsDatabase.markAsSent(messageId, true);
} catch (MmsException e) {
Log.w(TAG, e);
}
} else {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
RecipientId sender = Recipient.externalPush(context, editor, null).getId();
IncomingTextMessage incoming = new IncomingTextMessage(sender, -1, timestamp, timestamp, "", Optional.of(groupId), 0, false);
IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage(incoming, decryptedGroupV2Context);
smsDatabase.insertMessageInbox(groupMessage);
}
}
}

View File

@ -58,7 +58,6 @@ 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 {
@ -167,10 +166,9 @@ public class PushGroupSendJob extends PushSendJob {
List<RecipientId> target;
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 OutgoingGroupUpdateMessage) target = getGroupMessageV2Recipients((OutgoingGroupUpdateMessage) message);
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 target = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId);
List<SendMessageResult> results = deliver(message, groupRecipient, target);
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Recipient.externalPush(context, result.getAddress()).getId())).toList();
@ -330,24 +328,20 @@ public class PushGroupSendJob extends PushSendJob {
private @NonNull List<RecipientId> getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) {
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList();
if (!destinations.isEmpty()) {
return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList();
}
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
return Stream.of(members).map(Recipient::getId).toList();
}
List<RecipientId> members = Stream.of(DatabaseFactory.getGroupDatabase(context)
.getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF))
.map(Recipient::getId)
.toList();
private @NonNull List<RecipientId> getGroupMessageV2Recipients(@NonNull OutgoingGroupUpdateMessage 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();
if (members.size() > 0) {
Log.w(TAG, "No destinations found for group message " + groupId + " using current group membership");
}
return members;
}
public static class Factory implements Job.Factory<PushGroupSendJob> {

View File

@ -1,19 +1,24 @@
package org.thoughtcrime.securesms.mms;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.util.UUIDUtil;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
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.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@ -22,9 +27,10 @@ import java.util.UUID;
*/
public final class MessageGroupContext {
private final String encodedGroupContext;
private final GroupV1Properties groupV1;
private final GroupV2Properties groupV2;
@NonNull private final String encodedGroupContext;
@NonNull private final GroupProperties group;
@Nullable private final GroupV1Properties groupV1;
@Nullable private final GroupV2Properties groupV2;
public MessageGroupContext(@NonNull String encodedGroupContext, boolean v2)
throws IOException
@ -33,9 +39,11 @@ public final class MessageGroupContext {
if (v2) {
this.groupV1 = null;
this.groupV2 = new GroupV2Properties(DecryptedGroupV2Context.parseFrom(Base64.decode(encodedGroupContext)));
this.group = groupV2;
} else {
this.groupV1 = new GroupV1Properties(GroupContext.parseFrom(Base64.decode(encodedGroupContext)));
this.groupV2 = null;
this.group = groupV1;
}
}
@ -43,12 +51,14 @@ public final class MessageGroupContext {
this.encodedGroupContext = Base64.encodeBytes(group.toByteArray());
this.groupV1 = new GroupV1Properties(group);
this.groupV2 = null;
this.group = groupV1;
}
public MessageGroupContext(@NonNull DecryptedGroupV2Context group) {
this.encodedGroupContext = Base64.encodeBytes(group.toByteArray());
this.groupV1 = null;
this.groupV2 = new GroupV2Properties(group);
this.group = groupV2;
}
public @NonNull GroupV1Properties requireGroupV1Properties() {
@ -73,7 +83,20 @@ public final class MessageGroupContext {
return encodedGroupContext;
}
public static class GroupV1Properties {
public String getName() {
return group.getName();
}
public List<RecipientId> getMembersListExcludingSelf() {
return group.getMembersListExcludingSelf();
}
interface GroupProperties {
@NonNull String getName();
@NonNull List<RecipientId> getMembersListExcludingSelf();
}
public static class GroupV1Properties implements GroupProperties {
private final GroupContext groupContext;
@ -92,9 +115,32 @@ public final class MessageGroupContext {
public boolean isUpdate() {
return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
}
@Override
public @NonNull String getName() {
return groupContext.getName();
}
@Override
public @NonNull List<RecipientId> getMembersListExcludingSelf() {
List<GroupContext.Member> membersList = groupContext.getMembersList();
if (membersList.isEmpty()) {
return Collections.emptyList();
} else {
LinkedList<RecipientId> members = new LinkedList<>();
for (GroupContext.Member member : membersList) {
RecipientId recipient = RecipientId.from(UuidUtil.parseOrNull(member.getUuid()), member.getE164());
if (!Recipient.self().getId().equals(recipient)) {
members.add(recipient);
}
}
return members;
}
}
}
public static class GroupV2Properties {
public static class GroupV2Properties implements GroupProperties {
private final DecryptedGroupV2Context decryptedGroupV2Context;
private final GroupContextV2 groupContext;
@ -126,9 +172,32 @@ public final class MessageGroupContext {
return DecryptedGroupUtil.pendingToUuidList(decryptedGroupV2Context.getGroupState().getPendingMembersList());
}
public @NonNull List<UUID> getRemovedMembers() {
return DecryptedGroupUtil.removedMembersUuidList(decryptedGroupV2Context.getChange());
}
public boolean isUpdate() {
// The group context is only stored on update messages.
return true;
}
@Override
public @NonNull String getName() {
return decryptedGroupV2Context.getGroupState().getTitle();
}
@Override
public @NonNull List<RecipientId> getMembersListExcludingSelf() {
List<RecipientId> members = new ArrayList<>(decryptedGroupV2Context.getGroupState().getMembersCount());
for (DecryptedMember member : decryptedGroupV2Context.getGroupState().getMembersList()) {
RecipientId recipient = RecipientId.from(UuidUtil.fromByteString(member.getUuid()), null);
if (!Recipient.self().getId().equals(recipient)) {
members.add(recipient);
}
}
return members;
}
}
}

View File

@ -42,13 +42,11 @@ final class RecipientDialogRepository {
this.groupId = groupId;
}
@NonNull
RecipientId getRecipientId() {
@NonNull RecipientId getRecipientId() {
return recipientId;
}
@Nullable
GroupId getGroupId() {
@Nullable GroupId getGroupId() {
return groupId;
}

View File

@ -1,13 +1,24 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.mms.MessageGroupContext;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
public final class IncomingGroupUpdateMessage extends IncomingTextMessage {
private final GroupContext groupContext;
private final MessageGroupContext groupContext;
public IncomingGroupUpdateMessage(IncomingTextMessage base, GroupContext groupContext, String body) {
super(base, body);
this(base, new MessageGroupContext(groupContext));
}
public IncomingGroupUpdateMessage(IncomingTextMessage base, DecryptedGroupV2Context groupV2Context) {
this(base, new MessageGroupContext(groupV2Context));
}
public IncomingGroupUpdateMessage(IncomingTextMessage base, MessageGroupContext groupContext) {
super(base, groupContext.getEncodedGroupContext());
this.groupContext = groupContext;
}
@ -17,11 +28,15 @@ public final class IncomingGroupUpdateMessage extends IncomingTextMessage {
}
public boolean isUpdate() {
return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
return groupContext.isV2Group() || groupContext.requireGroupV1Properties().isUpdate();
}
public boolean isGroupV2() {
return groupContext.isV2Group();
}
public boolean isQuit() {
return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
return !groupContext.isV2Group() && groupContext.requireGroupV1Properties().isQuit();
}
}

View File

@ -6,15 +6,13 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.groups.BadGroupIdException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.recipients.RecipientId;
@ -23,15 +21,10 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
public final class GroupUtil {
private GroupUtil() {
@ -74,34 +67,13 @@ public final class GroupUtil {
return Optional.absent();
}
@WorkerThread
public static Optional<OutgoingGroupUpdateMessage> createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) {
GroupId encodedGroupId = groupRecipient.requireGroupId();
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
if (!groupDatabase.isActive(encodedGroupId)) {
Log.w(TAG, "Group has already been left.");
return Optional.absent();
}
ByteString decodedGroupId = ByteString.copyFrom(encodedGroupId.getDecodedId());
GroupContext groupContext = GroupContext.newBuilder()
.setId(decodedGroupId)
.setType(GroupContext.Type.QUIT)
.build();
return Optional.of(new OutgoingGroupUpdateMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList()));
}
public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup) {
public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup, boolean isV2) {
if (encodedGroup == null) {
return new GroupDescription(context, null);
}
try {
GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup));
MessageGroupContext groupContext = new MessageGroupContext(encodedGroup, isV2);
return new GroupDescription(context, groupContext);
} catch (IOException e) {
Log.w(TAG, e);
@ -129,25 +101,19 @@ public final class GroupUtil {
public static class GroupDescription {
@NonNull private final Context context;
@Nullable private final GroupContext groupContext;
@Nullable private final List<RecipientId> members;
@NonNull private final Context context;
@Nullable private final MessageGroupContext groupContext;
@Nullable private final List<RecipientId> members;
GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) {
GroupDescription(@NonNull Context context, @Nullable MessageGroupContext groupContext) {
this.context = context.getApplicationContext();
this.groupContext = groupContext;
if (groupContext == null || groupContext.getMembersList().isEmpty()) {
if (groupContext == null) {
this.members = null;
} else {
this.members = new LinkedList<>();
for (GroupContext.Member member : groupContext.getMembersList()) {
RecipientId recipientId = RecipientId.from(UuidUtil.parseOrNull(member.getUuid()), member.getE164());
if (!recipientId.equals(Recipient.self().getId())) {
this.members.add(recipientId);
}
}
List<RecipientId> membersList = groupContext.getMembersListExcludingSelf();
this.members = membersList.isEmpty() ? null : membersList;
}
}

View File

@ -678,6 +678,7 @@
<string name="MediaRepository_all_media">All media</string>
<!-- MessageRecord -->
<string name="MessageRecord_unknown">Unknown</string>
<string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Received a message encrypted using an old version of Signal that is no longer supported. Please ask the sender to update to the most recent version and resend the message.</string>
<string name="MessageRecord_left_group">You have left the group.</string>
<string name="MessageRecord_you_updated_group">You updated the group.</string>
@ -695,6 +696,9 @@
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s.</string>
<!-- GV2 specific -->
<string name="MessageRecord_you_created_the_group">You created the group.</string>
<string name="MessageRecord_group_updated">Group updated.</string>
<!-- GV2 member additions -->
<string name="MessageRecord_you_added_s">You added %1$s.</string>
<string name="MessageRecord_s_added_s">%1$s added %2$s.</string>

View File

@ -63,12 +63,26 @@ public final class DecryptedGroupUtil {
return uuidList;
}
public static ArrayList<UUID> removedMembersUuidList(DecryptedGroupChange groupChange) {
ArrayList<UUID> uuidList = new ArrayList<>(groupChange.getDeleteMembersCount());
for (ByteString member : groupChange.getDeleteMembersList()) {
uuidList.add(toUuid(member));
}
return uuidList;
}
public static UUID toUuid(DecryptedMember member) {
return UUIDUtil.deserialize(member.getUuid().toByteArray());
return toUuid(member.getUuid());
}
public static UUID toUuid(DecryptedPendingMember member) {
return UUIDUtil.deserialize(member.getUuid().toByteArray());
return toUuid(member.getUuid());
}
private static UUID toUuid(ByteString member) {
return UUIDUtil.deserialize(member.toByteArray());
}
/**
@ -90,6 +104,16 @@ public final class DecryptedGroupUtil {
return Optional.absent();
}
public static Optional<DecryptedMember> firstMember(Collection<DecryptedMember> members) {
Iterator<DecryptedMember> iterator = members.iterator();
if (iterator.hasNext()) {
return Optional.of(iterator.next());
} else {
return Optional.absent();
}
}
public static Optional<DecryptedPendingMember> findPendingByUuid(Collection<DecryptedPendingMember> members, UUID uuid) {
ByteString uuidBytes = UuidUtil.toByteString(uuid);