mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 20:18:34 +00:00
GroupsV2 update sending and local context storage.
This commit is contained in:
parent
f5e6fd6340
commit
78055e3ccb
@ -132,6 +132,21 @@ public final class GroupDatabase extends Database {
|
|||||||
return Optional.fromNullable(reader.getCurrent());
|
return Optional.fromNullable(reader.getCurrent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call if you are sure this group should exist.
|
||||||
|
* <p>
|
||||||
|
* Finds group and throws if it cannot.
|
||||||
|
*/
|
||||||
|
public @NonNull GroupRecord requireGroup(@NonNull GroupId groupId) {
|
||||||
|
Optional<GroupRecord> group = getGroup(groupId);
|
||||||
|
|
||||||
|
if (!group.isPresent()) {
|
||||||
|
throw new AssertionError("Group not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return group.get();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isUnknownGroup(@NonNull GroupId groupId) {
|
public boolean isUnknownGroup(@NonNull GroupId groupId) {
|
||||||
Optional<GroupRecord> group = getGroup(groupId);
|
Optional<GroupRecord> group = getGroup(groupId);
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
|||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||||
|
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||||
@ -791,7 +792,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
|
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)) {
|
} else if (Types.isExpirationTimerUpdate(outboxType)) {
|
||||||
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
||||||
}
|
}
|
||||||
@ -1050,8 +1051,16 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
|
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
|
||||||
|
|
||||||
if (message.isGroup()) {
|
if (message.isGroup()) {
|
||||||
if (((OutgoingGroupMediaMessage)message).isGroupUpdate()) type |= Types.GROUP_UPDATE_BIT;
|
OutgoingGroupMediaMessage outgoingGroupMediaMessage = (OutgoingGroupMediaMessage) message;
|
||||||
else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT;
|
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()) {
|
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);
|
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener);
|
||||||
|
|
||||||
if (message.getRecipient().isGroup()) {
|
if (message.getRecipient().isGroup()) {
|
||||||
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
OutgoingGroupMediaMessage outgoingGroupMediaMessage = (message instanceof OutgoingGroupMediaMessage) ? (OutgoingGroupMediaMessage) message : null;
|
||||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
|
||||||
|
|
||||||
receiptDatabase.insert(Stream.of(members).map(Recipient::getId).toList(),
|
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||||
messageId, defaultReceiptStatus, message.getSentTimeMillis());
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||||
|
Set<RecipientId> 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 : earlyDeliveryReceipts.keySet()) receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_DELIVERED, -1);
|
||||||
for (RecipientId recipientId : earlyReadReceipts.keySet()) receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_READ, -1);
|
for (RecipientId recipientId : earlyReadReceipts.keySet()) receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_READ, -1);
|
||||||
|
@ -76,6 +76,7 @@ public interface MmsSmsColumns {
|
|||||||
protected static final long GROUP_UPDATE_BIT = 0x10000;
|
protected static final long GROUP_UPDATE_BIT = 0x10000;
|
||||||
protected static final long GROUP_QUIT_BIT = 0x20000;
|
protected static final long GROUP_QUIT_BIT = 0x20000;
|
||||||
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
|
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
|
||||||
|
protected static final long GROUP_V2_BIT = 0x80000;
|
||||||
|
|
||||||
// Encrypted Storage Information XXX
|
// Encrypted Storage Information XXX
|
||||||
public static final long ENCRYPTION_MASK = 0xFF000000;
|
public static final long ENCRYPTION_MASK = 0xFF000000;
|
||||||
@ -223,6 +224,10 @@ public interface MmsSmsColumns {
|
|||||||
return (type & GROUP_UPDATE_BIT) != 0;
|
return (type & GROUP_UPDATE_BIT) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isGroupV2(long type) {
|
||||||
|
return (type & GROUP_V2_BIT) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isGroupQuit(long type) {
|
public static boolean isGroupQuit(long type) {
|
||||||
return (type & GROUP_QUIT_BIT) != 0;
|
return (type & GROUP_QUIT_BIT) != 0;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread;
|
|||||||
|
|
||||||
import com.annimon.stream.Collectors;
|
import com.annimon.stream.Collectors;
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
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.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
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.Preview;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Quote;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Quote;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
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.messages.shared.SharedContact;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class PushGroupSendJob extends PushSendJob {
|
public class PushGroupSendJob extends PushSendJob {
|
||||||
@ -147,24 +152,25 @@ public class PushGroupSendJob extends PushSendJob {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.getRecipient().isPushGroup()) {
|
Recipient groupRecipient = message.getRecipient().fresh();
|
||||||
|
|
||||||
|
if (!groupRecipient.isPushGroup()) {
|
||||||
throw new MmsException("Message recipient isn't a group!");
|
throw new MmsException("Message recipient isn't a group!");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log(TAG, "Sending message: " + messageId);
|
log(TAG, "Sending message: " + messageId);
|
||||||
|
|
||||||
if (!message.getRecipient().resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) {
|
if (!groupRecipient.resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) {
|
||||||
RecipientUtil.shareProfileIfFirstSecureMessage(context, message.getRecipient());
|
RecipientUtil.shareProfileIfFirstSecureMessage(context, groupRecipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RecipientId> target;
|
List<RecipientId> 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();
|
||||||
if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient).getId());
|
else if (groupRecipient.isPushV2Group() && message instanceof OutgoingGroupMediaMessage) target = getGroupMessageV2Recipients((OutgoingGroupMediaMessage) message);
|
||||||
else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).toList();
|
else target = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId);
|
||||||
else target = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId);
|
|
||||||
|
|
||||||
List<SendMessageResult> results = deliver(message, groupRecipient, target);
|
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();
|
List<NetworkFailure> 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();
|
.toList();
|
||||||
|
|
||||||
if (message.isGroup()) {
|
if (message.isGroup()) {
|
||||||
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
|
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
|
||||||
GroupContext groupContext = groupMessage.getGroupContext();
|
|
||||||
SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
|
if (groupMessage.isV2Group()) {
|
||||||
SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
|
MessageGroupContext.GroupV2Properties properties = groupMessage.requireGroupV2Properties();
|
||||||
List<SignalServiceAddress> members = Stream.of(groupContext.getMembersList())
|
GroupContextV2 groupContext = properties.getGroupContext();
|
||||||
.map(m -> new SignalServiceAddress(UuidUtil.parseOrNull(m.getUuid()), m.getE164()))
|
SignalServiceGroupV2.Builder builder = SignalServiceGroupV2.newBuilder(properties.getGroupMasterKey())
|
||||||
.toList();
|
.withRevision(groupContext.getRevision());
|
||||||
SignalServiceGroup group = new SignalServiceGroup(type, groupId.getDecodedId(), groupContext.getName(), members, avatar);
|
|
||||||
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
ByteString groupChange = groupContext.getGroupChange();
|
||||||
|
if (groupChange != null) {
|
||||||
|
builder.withSignedGroupChange(groupChange.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalServiceGroupV2 group = builder.build();
|
||||||
|
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
||||||
.withTimestamp(message.getSentTimeMillis())
|
.withTimestamp(message.getSentTimeMillis())
|
||||||
.withExpiration(groupRecipient.getExpireMessages())
|
.withExpiration(groupRecipient.getExpireMessages())
|
||||||
.asGroupMessage(group)
|
.asGroupMessage(group)
|
||||||
.build();
|
.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<SignalServiceAddress> 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 {
|
} else {
|
||||||
SignalServiceGroup group = new SignalServiceGroup(groupId.getDecodedId());
|
SignalServiceDataMessage.Builder builder = SignalServiceDataMessage.newBuilder()
|
||||||
SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
|
.withTimestamp(message.getSentTimeMillis());
|
||||||
.withTimestamp(message.getSentTimeMillis())
|
|
||||||
.asGroupMessage(group)
|
if (groupId.isV2()) {
|
||||||
.withAttachments(attachmentPointers)
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||||
.withBody(message.getBody())
|
GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties();
|
||||||
.withViewOnce(message.isViewOnce())
|
SignalServiceGroupV2 group = SignalServiceGroupV2.newBuilder(v2GroupProperties.getGroupMasterKey())
|
||||||
.asExpirationUpdate(message.isExpirationUpdate())
|
.withRevision(v2GroupProperties.getGroupRevision())
|
||||||
.withProfileKey(profileKey.orNull())
|
.build();
|
||||||
.withQuote(quote.orNull())
|
builder.asGroupMessage(group);
|
||||||
.withSticker(sticker.orNull())
|
} else {
|
||||||
.withSharedContacts(sharedContacts)
|
builder.asGroupMessage(new SignalServiceGroup(groupId.getDecodedId()));
|
||||||
.withPreviews(previews)
|
}
|
||||||
.build();
|
|
||||||
|
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);
|
return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupMessage);
|
||||||
}
|
}
|
||||||
@ -298,12 +339,27 @@ public class PushGroupSendJob extends PushSendJob {
|
|||||||
|
|
||||||
private @NonNull List<RecipientId> getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) {
|
private @NonNull List<RecipientId> getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) {
|
||||||
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(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);
|
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||||
return Stream.of(members).map(Recipient::getId).toList();
|
return Stream.of(members).map(Recipient::getId).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @NonNull List<RecipientId> 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<PushGroupSendJob> {
|
public static class Factory implements Job.Factory<PushGroupSendJob> {
|
||||||
@Override
|
@Override
|
||||||
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) {
|
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) {
|
||||||
|
@ -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<UUID> getActiveMembers() {
|
||||||
|
return DecryptedGroupUtil.membersToUuidList(decryptedGroupV2Context.getGroupState().getMembersList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull List<UUID> getPendingMembers() {
|
||||||
|
return DecryptedGroupUtil.pendingToUuidList(decryptedGroupV2Context.getGroupState().getPendingMembersList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUpdate() {
|
||||||
|
// The group context is only stored on update messages.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,21 +6,20 @@ import androidx.annotation.Nullable;
|
|||||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
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,
|
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||||
@NonNull String encodedGroupContext,
|
@NonNull MessageGroupContext groupContext,
|
||||||
@NonNull List<Attachment> avatar,
|
@NonNull List<Attachment> avatar,
|
||||||
long sentTimeMillis,
|
long sentTimeMillis,
|
||||||
long expiresIn,
|
long expiresIn,
|
||||||
@ -28,12 +27,11 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||||||
@Nullable QuoteModel quote,
|
@Nullable QuoteModel quote,
|
||||||
@NonNull List<Contact> contacts,
|
@NonNull List<Contact> contacts,
|
||||||
@NonNull List<LinkPreview> previews)
|
@NonNull List<LinkPreview> previews)
|
||||||
throws IOException
|
|
||||||
{
|
{
|
||||||
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
super(recipient, groupContext.getEncodedGroupContext(), avatar, sentTimeMillis,
|
||||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, viewOnce, quote, contacts, previews);
|
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, viewOnce, quote, contacts, previews);
|
||||||
|
|
||||||
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
this.messageGroupContext = groupContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||||
@ -46,12 +44,20 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||||||
@NonNull List<Contact> contacts,
|
@NonNull List<Contact> contacts,
|
||||||
@NonNull List<LinkPreview> previews)
|
@NonNull List<LinkPreview> previews)
|
||||||
{
|
{
|
||||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews);
|
||||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
}
|
||||||
System.currentTimeMillis(),
|
|
||||||
ThreadDatabase.DistributionTypes.CONVERSATION, 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<Contact> contacts,
|
||||||
|
@NonNull List<LinkPreview> previews)
|
||||||
|
{
|
||||||
|
this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -59,15 +65,19 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isGroupUpdate() {
|
public boolean isV2Group() {
|
||||||
return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
|
return messageGroupContext.isV2Group();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isGroupQuit() {
|
public @NonNull MessageGroupContext.GroupV1Properties requireGroupV1Properties() {
|
||||||
return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
|
return messageGroupContext.requireGroupV1Properties();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupContext getGroupContext() {
|
public @NonNull MessageGroupContext.GroupV2Properties requireGroupV2Properties() {
|
||||||
return group;
|
return messageGroupContext.requireGroupV2Properties();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Attachment> getAttachments(@Nullable Attachment avatar) {
|
||||||
|
return avatar == null ? Collections.emptyList() : Collections.singletonList(avatar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -575,6 +575,11 @@ public class Recipient {
|
|||||||
return groupId != null && groupId.isPush();
|
return groupId != null && groupId.isPush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPushV2Group() {
|
||||||
|
GroupId groupId = resolve().groupId;
|
||||||
|
return groupId != null && groupId.isV2();
|
||||||
|
}
|
||||||
|
|
||||||
public @NonNull List<Recipient> getParticipants() {
|
public @NonNull List<Recipient> getParticipants() {
|
||||||
return new ArrayList<>(participants);
|
return new ArrayList<>(participants);
|
||||||
}
|
}
|
||||||
|
@ -22,3 +22,13 @@ message ReactionList {
|
|||||||
|
|
||||||
repeated Reaction reactions = 1;
|
repeated Reaction reactions = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import "SignalService.proto";
|
||||||
|
import "DecryptedGroups.proto";
|
||||||
|
|
||||||
|
message DecryptedGroupV2Context {
|
||||||
|
signalservice.GroupContextV2 context = 1;
|
||||||
|
DecryptedGroupChange change = 2;
|
||||||
|
DecryptedGroup groupState = 3;
|
||||||
|
}
|
||||||
|
@ -39,6 +39,16 @@ public final class DecryptedGroupUtil {
|
|||||||
return uuidList;
|
return uuidList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ArrayList<UUID> membersToUuidList(Collection<DecryptedMember> membersList) {
|
||||||
|
ArrayList<UUID> uuidList = new ArrayList<>(membersList.size());
|
||||||
|
|
||||||
|
for (DecryptedMember member : membersList) {
|
||||||
|
uuidList.add(toUuid(member));
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuidList;
|
||||||
|
}
|
||||||
|
|
||||||
public static ArrayList<UUID> pendingToUuidList(Collection<DecryptedPendingMember> membersList) {
|
public static ArrayList<UUID> pendingToUuidList(Collection<DecryptedPendingMember> membersList) {
|
||||||
ArrayList<UUID> uuidList = new ArrayList<>(membersList.size());
|
ArrayList<UUID> uuidList = new ArrayList<>(membersList.size());
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user