Add message editing feature.

This commit is contained in:
Clark
2023-04-14 16:29:26 -04:00
committed by Cody Henthorne
parent 4f06a0d27c
commit 07f6baf7c1
73 changed files with 2051 additions and 304 deletions

View File

@@ -37,6 +37,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
@@ -105,6 +106,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRa
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.NullMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Preview;
@@ -421,6 +423,41 @@ public class SignalServiceMessageSender {
Content content = createMessageContent(message);
return sendContent(recipient, unidentifiedAccess, contentHint, message, sendEvents, urgent, includePniSignature, content);
}
/**
* Send an edit message to a single recipient.
*/
public SendMessageResult sendEditMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
ContentHint contentHint,
SignalServiceDataMessage message,
IndividualSendEvents sendEvents,
boolean urgent,
long targetSentTimestamp)
throws UntrustedIdentityException, IOException
{
Log.d(TAG, "[" + message.getTimestamp() + "] Sending an edit message.");
Content content = createEditMessageContent(new SignalServiceEditMessage(targetSentTimestamp, message));
return sendContent(recipient, unidentifiedAccess, contentHint, message, sendEvents, urgent, false, content);
}
/**
* Sends content to a single recipient.
*/
private SendMessageResult sendContent(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
ContentHint contentHint,
SignalServiceDataMessage message,
IndividualSendEvents sendEvents,
boolean urgent,
boolean includePniSignature,
Content content)
throws UntrustedIdentityException, IOException
{
if (includePniSignature) {
Log.d(TAG, "[" + message.getTimestamp() + "] Including PNI signature.");
content = content.toBuilder()
@@ -506,21 +543,29 @@ public class SignalServiceMessageSender {
/**
* Sends a {@link SignalServiceDataMessage} to a group using sender keys.
*/
public List<SendMessageResult> sendGroupDataMessage(DistributionId distributionId,
List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess,
boolean isRecipientUpdate,
ContentHint contentHint,
SignalServiceDataMessage message,
SenderKeyGroupEvents sendEvents,
boolean urgent,
boolean isForStory,
public List<SendMessageResult> sendGroupDataMessage(DistributionId distributionId,
List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess,
boolean isRecipientUpdate,
ContentHint contentHint,
SignalServiceDataMessage message,
SenderKeyGroupEvents sendEvents,
boolean urgent,
boolean isForStory,
SignalServiceEditMessage editMessage,
PartialSendBatchCompleteListener partialListener)
throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException
{
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a group data message to " + recipients.size() + " recipients using DistributionId " + distributionId);
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a group " + (editMessage != null ? "edit data message" : "data message") + " to " + recipients.size() + " recipients using DistributionId " + distributionId);
Content content;
if (editMessage != null) {
content = createEditMessageContent(editMessage);
} else {
content = createMessageContent(message);
}
Content content = createMessageContent(message);
Optional<byte[]> groupId = message.getGroupId();
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId, false, sendEvents, urgent, isForStory);
@@ -919,7 +964,23 @@ public class SignalServiceMessageSender {
}
private Content createMessageContent(SignalServiceDataMessage message) throws IOException {
Content.Builder container = Content.newBuilder();
Content.Builder container = Content.newBuilder();
DataMessage.Builder dataMessage = createDataMessage(message);
return enforceMaxContentSize(container.setDataMessage(dataMessage).build());
}
private Content createEditMessageContent(SignalServiceEditMessage editMessage) throws IOException {
Content.Builder container = Content.newBuilder();
DataMessage.Builder dataMessage = createDataMessage(editMessage.getDataMessage());
EditMessage.Builder editMessageProto = EditMessage.newBuilder()
.setDataMessage(dataMessage)
.setTargetSentTimestamp(editMessage.getTargetSentTimestamp());
return enforceMaxContentSize(container.setEditMessage(editMessageProto).build());
}
private DataMessage.Builder createDataMessage(SignalServiceDataMessage message) throws IOException {
DataMessage.Builder builder = DataMessage.newBuilder();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
@@ -1119,7 +1180,7 @@ public class SignalServiceMessageSender {
builder.setTimestamp(message.getTimestamp());
return enforceMaxContentSize(container.setDataMessage(builder).build());
return builder;
}
private Preview createPreview(SignalServicePreview preview) throws IOException {

View File

@@ -6,6 +6,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.whispersystems.signalservice.api.InvalidMessageStructureException
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
@@ -36,6 +37,7 @@ object EnvelopeContentValidator {
content.hasStoryMessage() -> validateStoryMessage(content.storyMessage)
content.hasPniSignatureMessage() -> Result.Valid
content.hasSenderKeyDistributionMessage() -> Result.Valid
content.hasEditMessage() -> validateEditMessage(content.editMessage)
else -> Result.Invalid("Content is empty!")
}
}
@@ -215,6 +217,43 @@ object EnvelopeContentValidator {
return Result.Valid
}
private fun validateEditMessage(editMessage: SignalServiceProtos.EditMessage): Result {
if (!editMessage.hasDataMessage()) {
return Result.Invalid("[EditMessage] No data message present")
}
if (!editMessage.hasTargetSentTimestamp()) {
return Result.Invalid("[EditMessage] No targetSentTimestamp specified")
}
val dataMessage: DataMessage = editMessage.dataMessage
if (dataMessage.requiredProtocolVersion > DataMessage.ProtocolVersion.CURRENT_VALUE) {
return Result.UnsupportedDataMessage(
ourVersion = DataMessage.ProtocolVersion.CURRENT_VALUE,
theirVersion = dataMessage.requiredProtocolVersion
)
}
if (dataMessage.previewList.any { it.hasImage() && it.image.isPresentAndInvalid() }) {
return Result.Invalid("[EditMessage] Invalid AttachmentPointer on DataMessage.previewList.image!")
}
if (dataMessage.bodyRangesList.any { it.hasMentionUuid() && it.mentionUuid.isNullOrInvalidUuid() }) {
return Result.Invalid("[EditMessage] Invalid UUID on body range!")
}
if (dataMessage.attachmentsList.any { it.isNullOrInvalid() }) {
return Result.Invalid("[EditMessage] Invalid attachments!")
}
if (dataMessage.hasGroupV2()) {
validateGroupContextV2(dataMessage.groupV2, "[EditMessage]")?.let { return it }
}
return Result.Valid
}
private fun AttachmentPointer?.isNullOrInvalid(): Boolean {
return this == null || this.attachmentIdentifierCase == AttachmentPointer.AttachmentIdentifierCase.ATTACHMENTIDENTIFIER_NOT_SET
}

View File

@@ -71,7 +71,8 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
@SuppressWarnings("OptionalIsPresent") public final class SignalServiceContent {
@SuppressWarnings("OptionalIsPresent")
public final class SignalServiceContent {
private static final String TAG = SignalServiceContent.class.getSimpleName();
@@ -95,6 +96,7 @@ import javax.annotation.Nullable;
private final Optional<DecryptionErrorMessage> decryptionErrorMessage;
private final Optional<SignalServiceStoryMessage> storyMessage;
private final Optional<SignalServicePniSignatureMessage> pniSignatureMessage;
private final Optional<SignalServiceEditMessage> editMessage;
private SignalServiceContent(SignalServiceDataMessage message,
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
@@ -130,6 +132,7 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.empty();
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.empty();
}
private SignalServiceContent(SignalServiceSyncMessage synchronizeMessage,
@@ -166,6 +169,7 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.empty();
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.empty();
}
private SignalServiceContent(SignalServiceCallMessage callMessage,
@@ -202,6 +206,7 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.empty();
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.empty();
}
private SignalServiceContent(SignalServiceReceiptMessage receiptMessage,
@@ -238,6 +243,7 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.empty();
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.empty();
}
private SignalServiceContent(DecryptionErrorMessage errorMessage,
@@ -274,6 +280,7 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.of(errorMessage);
this.storyMessage = Optional.empty();
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.empty();
}
private SignalServiceContent(SignalServiceTypingMessage typingMessage,
@@ -310,6 +317,7 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.empty();
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.empty();
}
private SignalServiceContent(SenderKeyDistributionMessage senderKeyDistributionMessage,
@@ -345,6 +353,7 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.empty();
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.empty();
}
private SignalServiceContent(SignalServicePniSignatureMessage pniSignatureMessage,
@@ -380,6 +389,7 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.empty();
this.pniSignatureMessage = Optional.of(pniSignatureMessage);
this.editMessage = Optional.empty();
}
private SignalServiceContent(SignalServiceStoryMessage storyMessage,
@@ -416,6 +426,44 @@ import javax.annotation.Nullable;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.of(storyMessage);
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.empty();
}
private SignalServiceContent(SignalServiceEditMessage editMessage,
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
Optional<SignalServicePniSignatureMessage> pniSignatureMessage,
SignalServiceAddress sender,
int senderDevice,
long timestamp,
long serverReceivedTimestamp,
long serverDeliveredTimestamp,
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
String destinationUuid,
SignalServiceContentProto serializedState)
{
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.serverReceivedTimestamp = serverReceivedTimestamp;
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.destinationUuid = destinationUuid;
this.serializedState = serializedState;
this.message = Optional.empty();
this.synchronizeMessage = Optional.empty();
this.callMessage = Optional.empty();
this.readMessage = Optional.empty();
this.typingMessage = Optional.empty();
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
this.decryptionErrorMessage = Optional.empty();
this.storyMessage = Optional.empty();
this.pniSignatureMessage = pniSignatureMessage;
this.editMessage = Optional.of(editMessage);
}
public Optional<SignalServiceDataMessage> getDataMessage() {
@@ -454,6 +502,10 @@ import javax.annotation.Nullable;
return pniSignatureMessage;
}
public Optional<SignalServiceEditMessage> getEditMessage() {
return editMessage;
}
public SignalServiceAddress getSender() {
return sender;
}
@@ -542,7 +594,7 @@ import javax.annotation.Nullable;
}
if (message.hasDataMessage()) {
return new SignalServiceContent(createSignalServiceMessage(metadata, message.getDataMessage()),
return new SignalServiceContent(createSignalServiceDataMessage(metadata, message.getDataMessage()),
senderKeyDistributionMessage,
pniSignatureMessage,
metadata.getSender(),
@@ -652,6 +704,20 @@ import javax.annotation.Nullable;
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (message.hasEditMessage()) {
return new SignalServiceContent(createEditMessage(metadata, message.getEditMessage()),
senderKeyDistributionMessage,
pniSignatureMessage,
metadata.getSender(),
metadata.getSenderDevice(),
metadata.getTimestamp(),
metadata.getServerReceivedTimestamp(),
metadata.getServerDeliveredTimestamp(),
false,
metadata.getServerGuid(),
metadata.getGroupId(),
metadata.getDestinationUuid(),
serviceContentProto);
} else if (senderKeyDistributionMessage.isPresent()) {
// IMPORTANT: This block should always be last, since you can pair SKDM's with other content
return new SignalServiceContent(senderKeyDistributionMessage.get(),
@@ -672,8 +738,8 @@ import javax.annotation.Nullable;
return null;
}
private static SignalServiceDataMessage createSignalServiceMessage(SignalServiceMetadata metadata,
SignalServiceProtos.DataMessage content)
private static SignalServiceDataMessage createSignalServiceDataMessage(SignalServiceMetadata metadata,
SignalServiceProtos.DataMessage content)
throws UnsupportedDataMessageException, InvalidMessageStructureException
{
SignalServiceGroupV2 groupInfoV2 = createGroupV2Info(content);
@@ -757,7 +823,7 @@ import javax.annotation.Nullable;
if (content.hasSent()) {
Map<ServiceId, Boolean> unidentifiedStatuses = new HashMap<>();
SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent();
Optional<SignalServiceDataMessage> dataMessage = sentContent.hasMessage() ? Optional.of(createSignalServiceMessage(metadata, sentContent.getMessage())) : Optional.empty();
Optional<SignalServiceDataMessage> dataMessage = sentContent.hasMessage() ? Optional.of(createSignalServiceDataMessage(metadata, sentContent.getMessage())) : Optional.empty();
Optional<SignalServiceStoryMessage> storyMessage = sentContent.hasStoryMessage() ? Optional.of(createStoryMessage(sentContent.getStoryMessage())) : Optional.empty();
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid())
? Optional.of(new SignalServiceAddress(ServiceId.parseOrThrow(sentContent.getDestinationUuid()), sentContent.getDestinationE164()))
@@ -1105,6 +1171,14 @@ import javax.annotation.Nullable;
}
}
private static SignalServiceEditMessage createEditMessage(SignalServiceMetadata metadata, SignalServiceProtos.EditMessage content) throws InvalidMessageStructureException, UnsupportedDataMessageException {
if (content.hasDataMessage() && content.getTargetSentTimestamp() != 0) {
return new SignalServiceEditMessage(content.getTargetSentTimestamp(), createSignalServiceDataMessage(metadata, content.getDataMessage()));
} else {
throw new InvalidMessageStructureException("Missing data message or timestamp from edit message.");
}
}
private static @Nullable SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content, boolean isGroupV2)
throws InvalidMessageStructureException
{

View File

@@ -0,0 +1,6 @@
package org.whispersystems.signalservice.api.messages
data class SignalServiceEditMessage(
val targetSentTimestamp: Long,
val dataMessage: SignalServiceDataMessage
)

View File

@@ -51,6 +51,7 @@ message Content {
optional bytes decryptionErrorMessage = 8;
optional StoryMessage storyMessage = 9;
optional PniSignatureMessage pniSignatureMessage = 10;
optional EditMessage editMessage = 11;
}
message CallMessage {
@@ -772,4 +773,9 @@ message DecryptionErrorMessage {
message PniSignatureMessage {
optional bytes pni = 1;
optional bytes signature = 2;
}
message EditMessage {
optional uint64 targetSentTimestamp = 1;
optional DataMessage dataMessage = 2;
}