GV2 group context proto.
parent
20d1a93b09
commit
640c82d517
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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<>();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue