GV2 group context proto.

master
Alan Evans 2020-03-27 15:12:10 -03:00 committed by Greyson Parrelli
parent 20d1a93b09
commit 640c82d517
15 changed files with 351 additions and 172 deletions

View File

@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Collections; import java.util.Collections;
@ -46,22 +47,29 @@ import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
public class GroupMessageProcessor { public final class GroupV1MessageProcessor {
private static final String TAG = GroupMessageProcessor.class.getSimpleName(); private static final String TAG = Log.tag(GroupV1MessageProcessor.class);
public static @Nullable Long process(@NonNull Context context, public static @Nullable Long process(@NonNull Context context,
@NonNull SignalServiceContent content, @NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message, @NonNull SignalServiceDataMessage message,
boolean outgoing) boolean outgoing)
{ {
if (!message.getGroupInfo().isPresent() || message.getGroupInfo().get().getGroupId() == null) { SignalServiceGroupContext signalServiceGroupContext = message.getGroupContext().get();
Optional<SignalServiceGroup> groupV1 = signalServiceGroupContext.getGroupV1();
if (signalServiceGroupContext.getGroupV2().isPresent()) {
throw new AssertionError("Cannot process GV2");
}
if (!groupV1.isPresent() || groupV1.get().getGroupId() == null) {
Log.w(TAG, "Received group message with no id! Ignoring..."); Log.w(TAG, "Received group message with no id! Ignoring...");
return null; return null;
} }
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
SignalServiceGroup group = message.getGroupInfo().get(); SignalServiceGroup group = groupV1.get();
GroupId id = GroupId.v1(group.getGroupId()); GroupId id = GroupId.v1(group.getGroupId());
Optional<GroupRecord> record = database.getGroup(id); Optional<GroupRecord> record = database.getGroup(id);
@ -278,7 +286,7 @@ public class GroupMessageProcessor {
.map(a -> a.getNumber().get()) .map(a -> a.getNumber().get())
.toList()); .toList());
builder.addAllMembers(Stream.of(group.getMembers().get()) builder.addAllMembers(Stream.of(group.getMembers().get())
.map(GroupMessageProcessor::createMember) .map(GroupV1MessageProcessor::createMember)
.toList()); .toList());
} }

View File

@ -120,7 +120,7 @@ final class V1GroupManager {
for (RecipientId member : members) { for (RecipientId member : members) {
Recipient recipient = Recipient.resolved(member); Recipient recipient = Recipient.resolved(member);
uuidMembers.add(GroupMessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient))); uuidMembers.add(GroupV1MessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient)));
} }
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder() GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()

View File

@ -29,13 +29,13 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -233,7 +233,7 @@ public final class PushDecryptMessageJob extends BaseJob {
return new PushProcessMessageJob.ExceptionMetadata(sender, return new PushProcessMessageJob.ExceptionMetadata(sender,
e.getSenderDevice(), e.getSenderDevice(),
e.getGroup().transform(g -> GroupId.v1(g.getGroupId())).orNull()); e.getGroup().transform(GroupUtil::idFromGroupContext).orNull());
} }
private static PushProcessMessageJob.ExceptionMetadata toExceptionMetadata(@NonNull ProtocolException e) throws NoSenderException { private static PushProcessMessageJob.ExceptionMetadata toExceptionMetadata(@NonNull ProtocolException e) throws NoSenderException {

View File

@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor; import org.thoughtcrime.securesms.groups.GroupV1MessageProcessor;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
@ -75,6 +75,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
@ -86,6 +87,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; 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.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
@ -263,17 +265,24 @@ public final class PushProcessMessageJob extends BaseJob {
if (content.getDataMessage().isPresent()) { if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get(); SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
boolean isGv2Message = groupId.isPresent() && groupId.get().isV2();
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), toEncodedId(message.getGroupInfo()), content.getTimestamp(), smsMessageId); if (isGv2Message) {
Log.w(TAG, "Ignoring GV2 message.");
return;
}
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId);
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId); else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
else if (message.getReaction().isPresent()) handleReaction(content, message); else if (message.getReaction().isPresent()) handleReaction(content, message);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId); else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId); else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupId.v1(message.getGroupInfo().get().getGroupId()))) { if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
handleUnknownGroupMessage(content, message.getGroupInfo().get()); handleUnknownGroupMessage(content, message.getGroupContext().get());
} }
if (message.getProfileKey().isPresent()) { if (message.getProfileKey().isPresent()) {
@ -327,10 +336,6 @@ public final class PushProcessMessageJob extends BaseJob {
} }
} }
private static @NonNull Optional<GroupId> toEncodedId(@NonNull Optional<SignalServiceGroup> groupInfo) {
return groupInfo.transform(g -> GroupId.v1(g.getGroupId()));
}
private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional<Long> smsMessageId) { private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional<Long> smsMessageId) {
switch (messageState) { switch (messageState) {
@ -415,7 +420,7 @@ public final class PushProcessMessageJob extends BaseJob {
{ {
Log.i(TAG, "handleCallIceUpdateMessage... " + messages.size()); Log.i(TAG, "handleCallIceUpdateMessage... " + messages.size());
ArrayList<IceCandidateParcel> iceCandidates = new ArrayList(messages.size()); ArrayList<IceCandidateParcel> iceCandidates = new ArrayList<>(messages.size());
long callId = -1; long callId = -1;
for (IceUpdateMessage iceMessage : messages) { for (IceUpdateMessage iceMessage : messages) {
iceCandidates.add(new IceCandidateParcel(iceMessage)); iceCandidates.add(new IceCandidateParcel(iceMessage));
@ -526,12 +531,12 @@ public final class PushProcessMessageJob extends BaseJob {
return threadId; return threadId;
} }
private void handleGroupMessage(@NonNull SignalServiceContent content, private void handleGroupV1Message(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message, @NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
throws StorageFailedException throws StorageFailedException
{ {
GroupMessageProcessor.process(context, content, message, false); GroupV1MessageProcessor.process(context, content, message, false);
if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getMessageDestination(content, message).getExpireMessages()) { if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getMessageDestination(content, message).getExpireMessages()) {
handleExpirationUpdate(content, message, Optional.absent()); handleExpirationUpdate(content, message, Optional.absent());
@ -543,12 +548,17 @@ public final class PushProcessMessageJob extends BaseJob {
} }
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content, private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceGroup group) @NonNull SignalServiceGroupContext group)
{ {
if (group.getType() != SignalServiceGroup.Type.REQUEST_INFO) { if (group.getGroupV1().isPresent()) {
ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(group.getGroupId()))); SignalServiceGroup groupV1 = group.getGroupV1().get();
if (groupV1.getType() != SignalServiceGroup.Type.REQUEST_INFO) {
ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(groupV1.getGroupId())));
} else {
Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring.");
}
} else { } else {
Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring."); Log.w(TAG, "Received a message for a group we don't know about without a GV1 context. Ignoring.");
} }
} }
@ -567,7 +577,7 @@ public final class PushProcessMessageJob extends BaseJob {
false, false,
content.isNeedsReceipt(), content.isNeedsReceipt(),
Optional.absent(), Optional.absent(),
message.getGroupInfo(), message.getGroupContext(),
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),
@ -729,8 +739,8 @@ public final class PushProcessMessageJob extends BaseJob {
handleGroupRecipientUpdate(message); handleGroupRecipientUpdate(message);
} else if (message.getMessage().isEndSession()) { } else if (message.getMessage().isEndSession()) {
threadId = handleSynchronizeSentEndSessionMessage(message); threadId = handleSynchronizeSentEndSessionMessage(message);
} else if (message.getMessage().isGroupUpdate()) { } else if (message.getMessage().isGroupV1Update()) {
threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true); threadId = GroupV1MessageProcessor.process(context, content, message.getMessage(), true);
} else if (message.getMessage().isExpirationUpdate()) { } else if (message.getMessage().isExpirationUpdate()) {
threadId = handleSynchronizeSentExpirationUpdate(message); threadId = handleSynchronizeSentExpirationUpdate(message);
} else if (message.getMessage().getReaction().isPresent()) { } else if (message.getMessage().getReaction().isPresent()) {
@ -743,8 +753,8 @@ public final class PushProcessMessageJob extends BaseJob {
threadId = handleSynchronizeSentTextMessage(message); threadId = handleSynchronizeSentTextMessage(message);
} }
if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupId.v1(message.getMessage().getGroupInfo().get().getGroupId()))) { if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) {
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get());
} }
if (message.getMessage().getProfileKey().isPresent()) { if (message.getMessage().getProfileKey().isPresent()) {
@ -854,7 +864,7 @@ public final class PushProcessMessageJob extends BaseJob {
message.isViewOnce(), message.isViewOnce(),
content.isNeedsReceipt(), content.isNeedsReceipt(),
message.getBody(), message.getBody(),
message.getGroupInfo(), message.getGroupContext(),
message.getAttachments(), message.getAttachments(),
quote, quote,
sharedContacts, sharedContacts,
@ -1040,7 +1050,8 @@ public final class PushProcessMessageJob extends BaseJob {
private void handleTextMessage(@NonNull SignalServiceContent content, private void handleTextMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message, @NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId,
@NonNull Optional<GroupId> groupId)
throws StorageFailedException throws StorageFailedException
{ {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context); SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
@ -1053,7 +1064,7 @@ public final class PushProcessMessageJob extends BaseJob {
Long threadId; Long threadId;
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { if (smsMessageId.isPresent() && !message.getGroupContext().isPresent()) {
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second; threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
} else { } else {
notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice()); notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice());
@ -1061,7 +1072,7 @@ public final class PushProcessMessageJob extends BaseJob {
IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(),
content.getSenderDevice(), content.getSenderDevice(),
message.getTimestamp(), body, message.getTimestamp(), body,
toEncodedId(message.getGroupInfo()), groupId,
message.getExpiresInSeconds() * 1000L, message.getExpiresInSeconds() * 1000L,
content.isNeedsReceipt()); content.isNeedsReceipt());
@ -1488,20 +1499,20 @@ public final class PushProcessMessageJob extends BaseJob {
return database.insertMessageInbox(textMessage); return database.insertMessageInbox(textMessage);
} }
private Recipient getSyncMessageDestination(SentTranscriptMessage message) { private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message) {
if (message.getMessage().getGroupInfo().isPresent()) { return getGroupRecipient(message.getMessage().getGroupContext())
return Recipient.externalGroup(context, GroupId.v1(message.getMessage().getGroupInfo().get().getGroupId())); .or(() -> Recipient.externalPush(context, message.getDestination().get()));
} else {
return Recipient.externalPush(context, message.getDestination().get());
}
} }
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) { private Recipient getMessageDestination(@NonNull SignalServiceContent content,
if (message.getGroupInfo().isPresent()) { @NonNull SignalServiceDataMessage message)
return Recipient.externalGroup(context, GroupId.v1(message.getGroupInfo().get().getGroupId())); {
} else { return getGroupRecipient(message.getGroupContext())
return Recipient.externalPush(context, content.getSender()); .or(() -> Recipient.externalPush(context, content.getSender()));
} }
private Optional<Recipient> getGroupRecipient(Optional<SignalServiceGroupContext> message) {
return message.transform(groupContext -> Recipient.externalGroup(context, GroupUtil.idFromGroupContext(groupContext)));
} }
private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull SignalServiceAddress sender, int device) { private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull SignalServiceAddress sender, int device) {
@ -1530,8 +1541,7 @@ public final class PushProcessMessageJob extends BaseJob {
return true; return true;
} else if (conversation.isGroup()) { } else if (conversation.isGroup()) {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Optional<GroupId> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupId.v1(message.getGroupInfo().get().getGroupId())) Optional<GroupId> groupId = message.getGroupContext().transform(GroupUtil::idFromGroupContext);
: Optional.absent();
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
return false; return false;
@ -1540,9 +1550,9 @@ public final class PushProcessMessageJob extends BaseJob {
boolean isTextMessage = message.getBody().isPresent(); boolean isTextMessage = message.getBody().isPresent();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent();
boolean isExpireMessage = message.isExpirationUpdate(); boolean isExpireMessage = message.isExpirationUpdate();
boolean isContentMessage = !message.isGroupUpdate() && !isExpireMessage && (isTextMessage || isMediaMessage); boolean isContentMessage = !message.isGroupV1Update() && !isExpireMessage && (isTextMessage || isMediaMessage);
boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get()); boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT; boolean isLeaveMessage = message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.QUIT;
return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage); return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage);
} else { } else {

View File

@ -8,9 +8,10 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
@ -68,7 +69,7 @@ public class IncomingMediaMessage {
boolean viewOnce, boolean viewOnce,
boolean unidentified, boolean unidentified,
Optional<String> body, Optional<String> body,
Optional<SignalServiceGroup> group, Optional<SignalServiceGroupContext> group,
Optional<List<SignalServiceAttachment>> attachments, Optional<List<SignalServiceAttachment>> attachments,
Optional<QuoteModel> quote, Optional<QuoteModel> quote,
Optional<List<Contact>> sharedContacts, Optional<List<Contact>> sharedContacts,
@ -86,7 +87,7 @@ public class IncomingMediaMessage {
this.quote = quote.orNull(); this.quote = quote.orNull();
this.unidentified = unidentified; this.unidentified = unidentified;
if (group.isPresent()) this.groupId = GroupId.v1(group.get().getGroupId()); if (group.isPresent()) this.groupId = GroupUtil.idFromGroupContext(group.get());
else this.groupId = null; else this.groupId = null;
this.attachments.addAll(PointerAttachment.forPointers(attachments)); this.attachments.addAll(PointerAttachment.forPointers(attachments));

View File

@ -1690,7 +1690,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
RemotePeer remotePeer = (RemotePeer)remote; RemotePeer remotePeer = (RemotePeer)remote;
Intent intent = new Intent(this, WebRtcCallService.class); Intent intent = new Intent(this, WebRtcCallService.class);
ArrayList<IceCandidateParcel> iceCandidateParcels = new ArrayList(iceCandidates.size()); ArrayList<IceCandidateParcel> iceCandidateParcels = new ArrayList<>(iceCandidates.size());
for (IceCandidate iceCandidate : iceCandidates) { for (IceCandidate iceCandidate : iceCandidates) {
iceCandidateParcels.add(new IceCandidateParcel(iceCandidate)); iceCandidateParcels.add(new IceCandidateParcel(iceCandidate));
} }

View File

@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
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;
@ -34,6 +35,26 @@ public final class GroupUtil {
private static final String TAG = Log.tag(GroupUtil.class); private static final String TAG = Log.tag(GroupUtil.class);
/**
* Result may be a v1 or v2 GroupId.
*/
public static GroupId idFromGroupContext(@NonNull SignalServiceGroupContext groupContext) {
if (groupContext.getGroupV1().isPresent()) {
return GroupId.v1(groupContext.getGroupV1().get().getGroupId());
} else if (groupContext.getGroupV2().isPresent()) {
return GroupId.v2(groupContext.getGroupV2().get().getMasterKey());
} else {
throw new AssertionError();
}
}
/**
* Result may be a v1 or v2 GroupId.
*/
public static @NonNull Optional<GroupId> idFromGroupContext(@NonNull Optional<SignalServiceGroupContext> groupContext) {
return groupContext.transform(GroupUtil::idFromGroupContext);
}
@WorkerThread @WorkerThread
public static Optional<OutgoingGroupMediaMessage> createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) { public static Optional<OutgoingGroupMediaMessage> createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) {
GroupId encodedGroupId = groupRecipient.requireGroupId(); GroupId encodedGroupId = groupRecipient.requireGroupId();

View File

@ -27,6 +27,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup; 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.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
@ -64,6 +66,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMe
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage;
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 org.whispersystems.signalservice.internal.push.SignalServiceProtos.NullMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.NullMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
@ -451,8 +454,15 @@ public class SignalServiceMessageSender {
builder.setBody(message.getBody().get()); builder.setBody(message.getBody().get());
} }
if (message.getGroupInfo().isPresent()) { if (message.getGroupContext().isPresent()) {
builder.setGroup(createGroupContent(message.getGroupInfo().get())); SignalServiceGroupContext groupContext = message.getGroupContext().get();
if (groupContext.getGroupV1().isPresent()) {
builder.setGroup(createGroupContent(groupContext.getGroupV1().get()));
}
if (groupContext.getGroupV2().isPresent()) {
builder.setGroupV2(createGroupContent(groupContext.getGroupV2().get()));
}
} }
if (message.isEndSession()) { if (message.isEndSession()) {
@ -966,6 +976,19 @@ public class SignalServiceMessageSender {
return builder.build(); return builder.build();
} }
private static GroupContextV2 createGroupContent(SignalServiceGroupV2 group) {
GroupContextV2.Builder builder = GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(group.getMasterKey().serialize()))
.setRevision(group.getRevision());
byte[] signedGroupChange = group.getSignedGroupChange();
if (signedGroupChange != null) {
builder.setGroupChange(ByteString.copyFrom(signedGroupChange));
}
return builder.build();
}
private List<DataMessage.Contact> createSharedContactContent(List<SharedContact> contacts) throws IOException { private List<DataMessage.Contact> createSharedContactContent(List<SharedContact> contacts) throws IOException {
List<DataMessage.Contact> results = new LinkedList<>(); List<DataMessage.Contact> results = new LinkedList<>();

View File

@ -11,6 +11,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.libsignal.metadata.ProtocolInvalidKeyException; import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
import org.signal.libsignal.metadata.ProtocolInvalidMessageException; import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.InvalidMessageException;
@ -253,7 +255,17 @@ public final class SignalServiceContent {
private static SignalServiceDataMessage createSignalServiceMessage(SignalServiceMetadata metadata, SignalServiceProtos.DataMessage content) private static SignalServiceDataMessage createSignalServiceMessage(SignalServiceMetadata metadata, SignalServiceProtos.DataMessage content)
throws ProtocolInvalidMessageException, UnsupportedDataMessageException throws ProtocolInvalidMessageException, UnsupportedDataMessageException
{ {
SignalServiceGroup groupInfo = createGroupInfo(content); SignalServiceGroup groupInfoV1 = createGroupV1Info(content);
SignalServiceGroupV2 groupInfoV2 = createGroupV2Info(content);
Optional<SignalServiceGroupContext> groupContext;
try {
groupContext = SignalServiceGroupContext.createOptional(groupInfoV1, groupInfoV2);
} catch (InvalidMessageException e) {
throw new ProtocolInvalidMessageException(e, null, 0);
}
List<SignalServiceAttachment> attachments = new LinkedList<>(); List<SignalServiceAttachment> attachments = new LinkedList<>();
boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE ) != 0); boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE ) != 0);
boolean expirationUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0); boolean expirationUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0);
@ -269,7 +281,7 @@ public final class SignalServiceContent {
content.getRequiredProtocolVersion(), content.getRequiredProtocolVersion(),
metadata.getSender().getIdentifier(), metadata.getSender().getIdentifier(),
metadata.getSenderDevice(), metadata.getSenderDevice(),
Optional.fromNullable(groupInfo)); groupContext);
} }
for (SignalServiceProtos.AttachmentPointer pointer : content.getAttachmentsList()) { for (SignalServiceProtos.AttachmentPointer pointer : content.getAttachmentsList()) {
@ -283,7 +295,7 @@ public final class SignalServiceContent {
} }
return new SignalServiceDataMessage(metadata.getTimestamp(), return new SignalServiceDataMessage(metadata.getTimestamp(),
groupInfo, groupInfoV1, groupInfoV2,
attachments, attachments,
content.getBody(), content.getBody(),
endSession, endSession,
@ -310,7 +322,7 @@ public final class SignalServiceContent {
? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164())) ? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164()))
: Optional.<SignalServiceAddress>absent(); : Optional.<SignalServiceAddress>absent();
if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) { if (!address.isPresent() && !dataMessage.getGroupContext().isPresent()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0); throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0);
} }
@ -739,7 +751,7 @@ public final class SignalServiceContent {
} }
private static SignalServiceGroup createGroupInfo(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { private static SignalServiceGroup createGroupV1Info(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
if (!content.hasGroup()) return null; if (!content.hasGroup()) return null;
SignalServiceGroup.Type type; SignalServiceGroup.Type type;
@ -799,4 +811,30 @@ public final class SignalServiceContent {
return new SignalServiceGroup(content.getGroup().getId().toByteArray()); return new SignalServiceGroup(content.getGroup().getId().toByteArray());
} }
private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
if (!content.hasGroupV2()) return null;
SignalServiceProtos.GroupContextV2 groupV2 = content.getGroupV2();
if (!groupV2.hasMasterKey()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("No GV2 master key on message"), null, 0);
}
if (!groupV2.hasRevision()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("No GV2 revision on message"), null, 0);
}
SignalServiceGroupV2.Builder builder;
try {
builder = SignalServiceGroupV2.newBuilder(new GroupMasterKey(groupV2.getMasterKey().toByteArray()))
.withRevision(groupV2.getRevision());
} catch (InvalidInputException e) {
throw new ProtocolInvalidMessageException(new InvalidMessageException(e), null, 0);
}
if (groupV2.hasGroupChange()) {
builder.withSignedGroupChange(groupV2.getGroupChange().toByteArray());
}
return builder.build();
}
} }

View File

@ -6,6 +6,7 @@
package org.whispersystems.signalservice.api.messages; package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
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;
@ -21,7 +22,7 @@ public class SignalServiceDataMessage {
private final long timestamp; private final long timestamp;
private final Optional<List<SignalServiceAttachment>> attachments; private final Optional<List<SignalServiceAttachment>> attachments;
private final Optional<String> body; private final Optional<String> body;
private final Optional<SignalServiceGroup> group; private final Optional<SignalServiceGroupContext> group;
private final Optional<byte[]> profileKey; private final Optional<byte[]> profileKey;
private final boolean endSession; private final boolean endSession;
private final boolean expirationUpdate; private final boolean expirationUpdate;
@ -34,101 +35,33 @@ public class SignalServiceDataMessage {
private final boolean viewOnce; private final boolean viewOnce;
private final Optional<Reaction> reaction; private final Optional<Reaction> reaction;
/**
* Construct a SignalServiceDataMessage with a body and no attachments.
*
* @param timestamp The sent timestamp.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, String body) {
this(timestamp, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage with a body and no attachments.
*
* @param timestamp The sent timestamp.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
*/
public SignalServiceDataMessage(long timestamp, String body, int expiresInSeconds) {
this(timestamp, (List<SignalServiceAttachment>)null, body, expiresInSeconds);
}
public SignalServiceDataMessage(final long timestamp, final SignalServiceAttachment attachment, final String body) {
this(timestamp, new LinkedList<SignalServiceAttachment>() {{add(attachment);}}, body);
}
/**
* Construct a SignalServiceDataMessage with a body and list of attachments.
*
* @param timestamp The sent timestamp.
* @param attachments The attachments.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, List<SignalServiceAttachment> attachments, String body) {
this(timestamp, attachments, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage with a body and list of attachments.
*
* @param timestamp The sent timestamp.
* @param attachments The attachments.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
*/
public SignalServiceDataMessage(long timestamp, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
this(timestamp, null, attachments, body, expiresInSeconds);
}
/**
* Construct a SignalServiceDataMessage group message with attachments and body.
*
* @param timestamp The sent timestamp.
* @param group The group information.
* @param attachments The attachments.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body) {
this(timestamp, group, attachments, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage group message with attachments and body.
*
* @param timestamp The sent timestamp.
* @param group The group information.
* @param attachments The attachments.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which a message should disappear after having been seen.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null, null, false, null);
}
/** /**
* Construct a SignalServiceDataMessage. * Construct a SignalServiceDataMessage.
* *
* @param timestamp The sent timestamp. * @param timestamp The sent timestamp.
* @param group The group information (or null if none). * @param group The group information (or null if none).
* @param groupV2 The group information (or null if none).
* @param attachments The attachments (or null if none). * @param attachments The attachments (or null if none).
* @param body The message contents. * @param body The message contents.
* @param endSession Flag indicating whether this message should close a session. * @param endSession Flag indicating whether this message should close a session.
* @param expiresInSeconds Number of seconds in which the message should disappear after being seen. * @param expiresInSeconds Number of seconds in which the message should disappear after being seen.
*/ */
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, SignalServiceDataMessage(long timestamp,
List<SignalServiceAttachment> attachments, SignalServiceGroup group, SignalServiceGroupV2 groupV2,
String body, boolean endSession, int expiresInSeconds, List<SignalServiceAttachment> attachments,
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate, String body, boolean endSession, int expiresInSeconds,
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews, boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
Sticker sticker, boolean viewOnce, Reaction reaction) Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
Sticker sticker, boolean viewOnce, Reaction reaction)
{ {
try {
this.group = SignalServiceGroupContext.createOptional(group, groupV2);
} catch (InvalidMessageException e) {
throw new AssertionError(e);
}
this.timestamp = timestamp; this.timestamp = timestamp;
this.body = Optional.fromNullable(body); this.body = Optional.fromNullable(body);
this.group = Optional.fromNullable(group);
this.endSession = endSession; this.endSession = endSession;
this.expiresInSeconds = expiresInSeconds; this.expiresInSeconds = expiresInSeconds;
this.expirationUpdate = expirationUpdate; this.expirationUpdate = expirationUpdate;
@ -184,9 +117,9 @@ public class SignalServiceDataMessage {
} }
/** /**
* @return The message group info (if any). * @return The message group context (if any).
*/ */
public Optional<SignalServiceGroup> getGroupInfo() { public Optional<SignalServiceGroupContext> getGroupContext() {
return group; return group;
} }
@ -202,8 +135,10 @@ public class SignalServiceDataMessage {
return profileKeyUpdate; return profileKeyUpdate;
} }
public boolean isGroupUpdate() { public boolean isGroupV1Update() {
return group.isPresent() && group.get().getType() != SignalServiceGroup.Type.DELIVER; return group.isPresent() &&
group.get().getGroupV1().isPresent() &&
group.get().getGroupV1().get().getType() != SignalServiceGroup.Type.DELIVER;
} }
public int getExpiresInSeconds() { public int getExpiresInSeconds() {
@ -244,18 +179,19 @@ public class SignalServiceDataMessage {
private List<SharedContact> sharedContacts = new LinkedList<>(); private List<SharedContact> sharedContacts = new LinkedList<>();
private List<Preview> previews = new LinkedList<>(); private List<Preview> previews = new LinkedList<>();
private long timestamp; private long timestamp;
private SignalServiceGroup group; private SignalServiceGroup group;
private String body; private SignalServiceGroupV2 groupV2;
private boolean endSession; private String body;
private int expiresInSeconds; private boolean endSession;
private boolean expirationUpdate; private int expiresInSeconds;
private byte[] profileKey; private boolean expirationUpdate;
private boolean profileKeyUpdate; private byte[] profileKey;
private Quote quote; private boolean profileKeyUpdate;
private Sticker sticker; private Quote quote;
private boolean viewOnce; private Sticker sticker;
private Reaction reaction; private boolean viewOnce;
private Reaction reaction;
private Builder() {} private Builder() {}
@ -265,10 +201,21 @@ public class SignalServiceDataMessage {
} }
public Builder asGroupMessage(SignalServiceGroup group) { public Builder asGroupMessage(SignalServiceGroup group) {
if (this.groupV2 != null) {
throw new AssertionError("Can not contain both V1 and V2 group contexts.");
}
this.group = group; this.group = group;
return this; return this;
} }
public Builder asGroupMessage(SignalServiceGroupV2 group) {
if (this.group != null) {
throw new AssertionError("Can not contain both V1 and V2 group contexts.");
}
this.groupV2 = group;
return this;
}
public Builder withAttachment(SignalServiceAttachment attachment) { public Builder withAttachment(SignalServiceAttachment attachment) {
this.attachments.add(attachment); this.attachments.add(attachment);
return this; return this;
@ -354,7 +301,7 @@ public class SignalServiceDataMessage {
public SignalServiceDataMessage build() { public SignalServiceDataMessage build() {
if (timestamp == 0) timestamp = System.currentTimeMillis(); if (timestamp == 0) timestamp = System.currentTimeMillis();
return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession, return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
expiresInSeconds, expirationUpdate, profileKey, expiresInSeconds, expirationUpdate, profileKey,
profileKeyUpdate, quote, sharedContacts, previews, profileKeyUpdate, quote, sharedContacts, previews,
sticker, viewOnce, reaction); sticker, viewOnce, reaction);

View File

@ -1,4 +1,4 @@
/** /*
* Copyright (C) 2014-2016 Open Whisper Systems * Copyright (C) 2014-2016 Open Whisper Systems
* *
* Licensed according to the LICENSE file in this repository. * Licensed according to the LICENSE file in this repository.

View File

@ -0,0 +1,59 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
public final class SignalServiceGroupContext {
private final Optional<SignalServiceGroup> groupV1;
private final Optional<SignalServiceGroupV2> groupV2;
private SignalServiceGroupContext(SignalServiceGroup groupV1) {
this.groupV1 = Optional.of(groupV1);
this.groupV2 = Optional.absent();
}
private SignalServiceGroupContext(SignalServiceGroupV2 groupV2) {
this.groupV1 = Optional.absent();
this.groupV2 = Optional.of(groupV2);
}
public Optional<SignalServiceGroup> getGroupV1() {
return groupV1;
}
public Optional<SignalServiceGroupV2> getGroupV2() {
return groupV2;
}
static Optional<SignalServiceGroupContext> createOptional(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
throws InvalidMessageException
{
return Optional.fromNullable(create(groupV1, groupV2));
}
public static SignalServiceGroupContext create(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
throws InvalidMessageException
{
if (groupV1 == null && groupV2 == null) {
return null;
}
if (groupV1 != null && groupV2 != null) {
throw new InvalidMessageException("Message cannot have both V1 and V2 group contexts.");
}
if (groupV1 != null) {
return new SignalServiceGroupContext(groupV1);
} else {
return new SignalServiceGroupContext(groupV2);
}
}
public SignalServiceGroup.Type getGroupV1Type() {
if (groupV1.isPresent()) {
return groupV1.get().getType();
}
return null;
}
}

View File

@ -0,0 +1,66 @@
package org.whispersystems.signalservice.api.messages;
import org.signal.zkgroup.groups.GroupMasterKey;
/**
* Group information to include in SignalServiceMessages destined to v2 groups.
* <p>
* This class represents a "context" that is included with Signal Service messages
* to make them group messages.
*/
public final class SignalServiceGroupV2 {
private final GroupMasterKey masterKey;
private final int revision;
private final byte[] signedGroupChange;
private SignalServiceGroupV2(Builder builder) {
this.masterKey = builder.masterKey;
this.revision = builder.revision;
this.signedGroupChange = builder.signedGroupChange != null ? builder.signedGroupChange.clone() : null;
}
public GroupMasterKey getMasterKey() {
return masterKey;
}
public int getRevision() {
return revision;
}
public byte[] getSignedGroupChange() {
return signedGroupChange;
}
public static Builder newBuilder(GroupMasterKey masterKey) {
return new Builder(masterKey);
}
public static class Builder {
private final GroupMasterKey masterKey;
private int revision;
private byte[] signedGroupChange;
private Builder(GroupMasterKey masterKey) {
if (masterKey == null) {
throw new IllegalArgumentException();
}
this.masterKey = masterKey;
}
Builder withRevision(int revision) {
this.revision = revision;
return this;
}
Builder withSignedGroupChange(byte[] signedGroupChange) {
this.signedGroupChange = signedGroupChange;
return this;
}
public SignalServiceGroupV2 build() {
return new SignalServiceGroupV2(this);
}
}
}

View File

@ -1,8 +1,7 @@
package org.whispersystems.signalservice.internal.push; package org.whispersystems.signalservice.internal.push;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
/** /**
* Exception that indicates that the data message has a higher required protocol version than the * Exception that indicates that the data message has a higher required protocol version than the
@ -10,16 +9,16 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
*/ */
public class UnsupportedDataMessageException extends Exception { public class UnsupportedDataMessageException extends Exception {
private final int requiredVersion; private final int requiredVersion;
private final String sender; private final String sender;
private final int senderDevice; private final int senderDevice;
private final Optional<SignalServiceGroup> group; private final Optional<SignalServiceGroupContext> group;
public UnsupportedDataMessageException(int currentVersion, public UnsupportedDataMessageException(int currentVersion,
int requiredVersion, int requiredVersion,
String sender, String sender,
int senderDevice, int senderDevice,
Optional<SignalServiceGroup> group) Optional<SignalServiceGroupContext> group)
{ {
super("Required version: " + requiredVersion + ", Our version: " + currentVersion); super("Required version: " + requiredVersion + ", Our version: " + currentVersion);
this.requiredVersion = requiredVersion; this.requiredVersion = requiredVersion;
@ -40,7 +39,7 @@ public class UnsupportedDataMessageException extends Exception {
return senderDevice; return senderDevice;
} }
public Optional<SignalServiceGroup> getGroup() { public Optional<SignalServiceGroupContext> getGroup() {
return group; return group;
} }
} }

View File

@ -198,6 +198,7 @@ message DataMessage {
optional string body = 1; optional string body = 1;
repeated AttachmentPointer attachments = 2; repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3; optional GroupContext group = 3;
optional GroupContextV2 groupV2 = 15;
optional uint32 flags = 4; optional uint32 flags = 4;
optional uint32 expireTimer = 5; optional uint32 expireTimer = 5;
optional bytes profileKey = 6; optional bytes profileKey = 6;
@ -412,6 +413,12 @@ message GroupContext {
optional AttachmentPointer avatar = 5; optional AttachmentPointer avatar = 5;
} }
message GroupContextV2 {
optional bytes masterKey = 1;
optional uint32 revision = 2;
optional bytes groupChange = 3;
}
message ContactDetails { message ContactDetails {
message Avatar { message Avatar {
optional string contentType = 1; optional string contentType = 1;