mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add message editing feature.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.whispersystems.signalservice.api.messages
|
||||
|
||||
data class SignalServiceEditMessage(
|
||||
val targetSentTimestamp: Long,
|
||||
val dataMessage: SignalServiceDataMessage
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user