GV2 message contexts.
parent
f099c3591c
commit
b8df90531f
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue