"name":"java.lang.Long",
"allDeclaredFields":true,
"allDeclaredMethods":true,
- "allDeclaredConstructors":true
+ "allDeclaredConstructors":true,
+ "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.Number",
{"name":"viewOnce","parameterTypes":[] }
]
},
+{
+ "name":"org.asamk.signal.json.JsonEditMessage",
+ "allDeclaredFields":true,
+ "queryAllDeclaredMethods":true,
+ "queryAllDeclaredConstructors":true,
+ "methods":[
+ {"name":"dataMessage","parameterTypes":[] },
+ {"name":"targetSentTimestamp","parameterTypes":[] }
+ ]
+},
{
"name":"org.asamk.signal.json.JsonError",
"allDeclaredFields":true,
"allDeclaredMethods":true,
- "allDeclaredConstructors":true
+ "allDeclaredConstructors":true,
+ "methods":[
+ {"name":"message","parameterTypes":[] },
+ {"name":"type","parameterTypes":[] }
+ ]
},
{
"name":"org.asamk.signal.json.JsonGroupInfo",
"methods":[
{"name":"callMessage","parameterTypes":[] },
{"name":"dataMessage","parameterTypes":[] },
+ {"name":"editMessage","parameterTypes":[] },
{"name":"receiptMessage","parameterTypes":[] },
{"name":"source","parameterTypes":[] },
{"name":"sourceDevice","parameterTypes":[] },
"name":"org.asamk.signal.json.JsonSticker",
"allDeclaredFields":true,
"allDeclaredMethods":true,
- "allDeclaredConstructors":true
+ "allDeclaredConstructors":true,
+ "methods":[
+ {"name":"packId","parameterTypes":[] },
+ {"name":"stickerId","parameterTypes":[] }
+ ]
},
{
"name":"org.asamk.signal.json.JsonStoryContext",
{"name":"dataMessage","parameterTypes":[] },
{"name":"destination","parameterTypes":[] },
{"name":"destinationNumber","parameterTypes":[] },
- {"name":"destinationUuid","parameterTypes":[] }
+ {"name":"destinationUuid","parameterTypes":[] },
+ {"name":"editMessage","parameterTypes":[] }
]
},
{
"name":"org.whispersystems.signalservice.api.groupsv2.CredentialResponse",
"allDeclaredFields":true,
"allDeclaredMethods":true,
- "allDeclaredConstructors":true
+ "allDeclaredConstructors":true,
+ "methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential",
"allDeclaredFields":true,
"allDeclaredMethods":true,
- "allDeclaredConstructors":true
+ "allDeclaredConstructors":true,
+ "methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"
{"name":"sentTimestamp_"}
]
},
+{
+ "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$EditMessage",
+ "fields":[
+ {"name":"bitField0_"},
+ {"name":"dataMessage_"},
+ {"name":"targetSentTimestamp_"}
+ ]
+},
{
"name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Envelope",
"fields":[
Message message, Set<RecipientIdentifier> recipients
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
+ SendMessageResults sendEditMessage(
+ Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
+ ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
+
SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
private SendMessageResults sendMessage(
SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ return sendMessage(messageBuilder, recipients, Optional.empty());
+ }
+
+ private SendMessageResults sendMessage(
+ SignalServiceDataMessage.Builder messageBuilder,
+ Set<RecipientIdentifier> recipients,
+ Optional<Long> editTargetTimestamp
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = System.currentTimeMillis();
if (recipient instanceof RecipientIdentifier.Single single) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(single);
- final var result = context.getSendHelper().sendMessage(messageBuilder, recipientId);
+ final var result = context.getSendHelper()
+ .sendMessage(messageBuilder, recipientId, editTargetTimestamp);
results.put(recipient, List.of(toSendMessageResult(result)));
} catch (UnregisteredRecipientException e) {
results.put(recipient,
List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
}
} else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
- final var result = context.getSendHelper().sendSelfMessage(messageBuilder);
+ final var result = context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
results.put(recipient, List.of(toSendMessageResult(result)));
} else if (recipient instanceof RecipientIdentifier.Group group) {
- final var result = context.getSendHelper().sendAsGroupMessage(messageBuilder, group.groupId());
+ final var result = context.getSendHelper()
+ .sendAsGroupMessage(messageBuilder, group.groupId(), editTargetTimestamp);
results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
}
}
return sendMessage(messageBuilder, recipients);
}
+ @Override
+ public SendMessageResults sendEditMessage(
+ Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
+ ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
+ final var messageBuilder = SignalServiceDataMessage.newBuilder();
+ applyMessage(messageBuilder, message);
+ return sendMessage(messageBuilder, recipients, Optional.of(editTargetTimestamp));
+ }
+
private void applyMessage(
final SignalServiceDataMessage.Builder messageBuilder, final Message message
) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
Optional<Receipt> receipt,
Optional<Typing> typing,
Optional<Data> data,
+ Optional<Edit> edit,
Optional<Sync> sync,
Optional<Call> call,
Optional<Story> story
}
}
+ public record Edit(long targetSentTimestamp, Data dataMessage) {
+
+ public static Edit from(
+ final SignalServiceEditMessage editMessage,
+ RecipientResolver recipientResolver,
+ RecipientAddressResolver addressResolver,
+ final AttachmentFileProvider fileProvider
+ ) {
+ return new Edit(editMessage.getTargetSentTimestamp(),
+ Data.from(editMessage.getDataMessage(), recipientResolver, addressResolver, fileProvider));
+ }
+ }
+
public record Sync(
Optional<Sent> sent,
Optional<Blocked> blocked,
Optional<RecipientAddress> destination,
Set<RecipientAddress> recipients,
Optional<Data> message,
+ Optional<Edit> editMessage,
Optional<Story> story
) {
.collect(Collectors.toSet()),
sentMessage.getDataMessage()
.map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)),
+ sentMessage.getEditMessage()
+ .map(message -> Edit.from(message, recipientResolver, addressResolver, fileProvider)),
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
}
}
Optional<Receipt> receipt;
Optional<Typing> typing;
Optional<Data> data;
+ Optional<Edit> edit;
Optional<Sync> sync;
Optional<Call> call;
Optional<Story> story;
typing = content.getTypingMessage().map(Typing::from);
data = content.getDataMessage()
.map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider));
+ edit = content.getEditMessage().map(s -> Edit.from(s, recipientResolver, addressResolver, fileProvider));
sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider));
call = content.getCallMessage().map(Call::from);
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
List.of(envelope.getTimestamp()))) : Optional.empty();
typing = Optional.empty();
data = Optional.empty();
+ edit = Optional.empty();
sync = Optional.empty();
call = Optional.empty();
story = Optional.empty();
receipt,
typing,
data,
+ edit,
sync,
call,
story);
private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
- context.getSendHelper().sendAsGroupMessage(messageBuilder, groupId);
+ context.getSendHelper().sendAsGroupMessage(messageBuilder, groupId, Optional.empty());
}
private SendGroupMessageResults updateGroupV2(
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
+import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
* The message is extended with the current expiration timer.
*/
public SendMessageResult sendMessage(
- final SignalServiceDataMessage.Builder messageBuilder, final RecipientId recipientId
- ) throws IOException {
+ final SignalServiceDataMessage.Builder messageBuilder,
+ final RecipientId recipientId,
+ Optional<Long> editTargetTimestamp
+ ) {
var contact = account.getContactStore().getContact(recipientId);
if (contact == null || !contact.isProfileSharingEnabled()) {
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
}
final var message = messageBuilder.build();
- return sendMessage(message, recipientId);
+ return sendMessage(message, recipientId, editTargetTimestamp);
}
/**
* The message is extended with the current expiration timer for the group and the group context.
*/
public List<SendMessageResult> sendAsGroupMessage(
- SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
+ SignalServiceDataMessage.Builder messageBuilder, GroupId groupId, Optional<Long> editTargetTimestamp
) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
final var g = getGroupForSending(groupId);
- return sendAsGroupMessage(messageBuilder, g);
+ return sendAsGroupMessage(messageBuilder, g, editTargetTimestamp);
}
/**
final Set<RecipientId> recipientIds,
final DistributionId distributionId
) throws IOException {
- return sendGroupMessage(message, recipientIds, distributionId, ContentHint.IMPLICIT);
+ return sendGroupMessage(message, recipientIds, distributionId, ContentHint.IMPLICIT, Optional.empty());
}
public SendMessageResult sendReceiptMessage(
}
public SendMessageResult sendSelfMessage(
- SignalServiceDataMessage.Builder messageBuilder
+ SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp
) {
final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId);
messageBuilder.withExpiration(expirationTime);
var message = messageBuilder.build();
- return sendSelfMessage(message);
+ return sendSelfMessage(message, editTargetTimestamp);
}
public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) {
}
private List<SendMessageResult> sendAsGroupMessage(
- final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
+ final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g, Optional<Long> editTargetTimestamp
) throws IOException, GroupSendingNotAllowedException {
GroupUtils.setGroupContext(messageBuilder, g);
messageBuilder.withExpiration(g.getMessageExpirationTimer());
}
}
- return sendGroupMessage(message, recipients, g.getDistributionId(), ContentHint.RESENDABLE);
+ return sendGroupMessage(message,
+ recipients,
+ g.getDistributionId(),
+ ContentHint.RESENDABLE,
+ editTargetTimestamp);
}
private List<SendMessageResult> sendGroupMessage(
final SignalServiceDataMessage message,
final Set<RecipientId> recipientIds,
final DistributionId distributionId,
- final ContentHint contentHint
+ final ContentHint contentHint,
+ final Optional<Long> editTargetTimestamp
) throws IOException {
final var messageSender = dependencies.getMessageSender();
final var messageSendLogStore = account.getMessageSendLogStore();
SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY,
urgent,
false,
- null,
+ editTargetTimestamp.map(timestamp -> new SignalServiceEditMessage(timestamp, message)).orElse(null),
sendResult -> {
logger.trace("Partial message send results: {}", sendResult.size());
synchronized (entryId) {
}
private SendMessageResult sendMessage(
- SignalServiceDataMessage message, RecipientId recipientId
+ SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
) {
final var messageSendLogStore = account.getMessageSendLogStore();
final var urgent = true;
+ final var includePniSignature = false;
final var result = handleSendMessage(recipientId,
- (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
+ editTargetTimestamp.isEmpty()
+ ? (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
unidentifiedAccess,
ContentHint.RESENDABLE,
message,
SignalServiceMessageSender.IndividualSendEvents.EMPTY,
urgent,
- false));
+ includePniSignature)
+ : (messageSender, address, unidentifiedAccess) -> messageSender.sendEditMessage(address,
+ unidentifiedAccess,
+ ContentHint.RESENDABLE,
+ message,
+ SignalServiceMessageSender.IndividualSendEvents.EMPTY,
+ urgent,
+ editTargetTimestamp.get()));
messageSendLogStore.insertIfPossible(message.getTimestamp(), result, ContentHint.RESENDABLE, urgent);
handleSendMessageResult(result);
return result;
}
}
- private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) {
+ private SendMessageResult sendSelfMessage(SignalServiceDataMessage message, Optional<Long> editTargetTimestamp) {
var address = account.getSelfAddress();
var transcript = new SentTranscriptMessage(Optional.of(address),
message.getTimestamp(),
- Optional.of(message),
+ editTargetTimestamp.isEmpty() ? Optional.of(message) : Optional.empty(),
message.getExpiresInSeconds(),
Map.of(address.getServiceId(), true),
false,
Optional.empty(),
Set.of(),
- Optional.empty());
+ editTargetTimestamp.map((timestamp) -> new SignalServiceEditMessage(timestamp, message)));
var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
return sendSyncMessage(syncMessage);
*-e*, *--end-session*::
Clear session state and send end session message.
+*--edit-timestamp*::
+Specify the timestamp of a previous message with the recipient or group to send an edited message.
+
=== sendPaymentNotification
Send a payment notification.
var message = envelope.data().get();
printDataMessage(writer, message);
}
+ if (envelope.edit().isPresent()) {
+ var message = envelope.edit().get();
+ printEditMessage(writer, message);
+ }
if (envelope.story().isPresent()) {
var message = envelope.story().get();
printStoryMessage(writer.indentedWriter(), message);
}
}
+ private void printEditMessage(
+ PlainTextWriter writer, MessageEnvelope.Edit message
+ ) {
+ writer.println("Edit: Target message timestamp: {}", DateUtils.formatTimestamp(message.targetSentTimestamp()));
+ printDataMessage(writer.indentedWriter(), message.dataMessage());
+ }
+
private void printStoryMessage(
PlainTextWriter writer, MessageEnvelope.Story message
) {
.type(long.class)
.help("Specify the timestamp of a story to reply to.");
subparser.addArgument("--story-author").help("Specify the number of the author of the story.");
+ subparser.addArgument("--edit-timestamp")
+ .type(long.class)
+ .help("Specify the timestamp of a previous message with the recipient or group to send an edited message.");
}
@Override
"Sending empty message is not allowed, either a message, attachment or sticker must be given.");
}
+ final var editTimestamp = ns.getLong("edit-timestamp");
+
try {
final var message = new Message(messageText,
attachments,
Optional.ofNullable(sticker),
previews,
Optional.ofNullable((storyReply)));
- var results = m.sendMessage(message, recipientIdentifiers);
+ var results = editTimestamp != null
+ ? m.sendEditMessage(message, recipientIdentifiers, editTimestamp)
+ : m.sendMessage(message, recipientIdentifiers);
outputResult(outputWriter, results);
} catch (AttachmentInvalidException | IOException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
+import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.StickerPackInvalidException;
import org.asamk.signal.manager.api.StickerPackUrl;
import org.asamk.signal.manager.api.TypingAction;
+import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus;
groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
}
+ @Override
+ public SendMessageResults sendEditMessage(
+ final Message message, final Set<RecipientIdentifier> recipients, final long editTargetTimestamp
+ ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public SendMessageResults sendRemoteDeleteMessage(
final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
List.of())),
Optional.empty(),
Optional.empty(),
+ Optional.empty(),
Optional.empty());
notifyMessageHandlers(envelope);
};
Optional.empty(),
Optional.empty(),
Optional.empty(),
+ Optional.empty(),
Optional.empty());
notifyMessageHandlers(envelope);
};
Optional.empty(),
Optional.empty(),
Optional.empty(),
+ Optional.empty(),
Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
syncReceived.getTimestamp(),
syncReceived.getDestination().isEmpty()
List.of(),
List.of(),
List.of())),
+ Optional.empty(),
Optional.empty())),
Optional.empty(),
List.of(),
--- /dev/null
+package org.asamk.signal.json;
+
+import org.asamk.signal.manager.api.MessageEnvelope;
+
+record JsonEditMessage(long targetSentTimestamp, JsonDataMessage dataMessage) {
+
+ static JsonEditMessage from(MessageEnvelope.Edit editMessage) {
+ return new JsonEditMessage(editMessage.targetSentTimestamp(), JsonDataMessage.from(editMessage.dataMessage()));
+ }
+}
Integer sourceDevice,
long timestamp,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonDataMessage dataMessage,
+ @JsonInclude(JsonInclude.Include.NON_NULL) JsonEditMessage editMessage,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonStoryMessage storyMessage,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonSyncMessage syncMessage,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonCallMessage callMessage,
final var typingMessage = envelope.typing().map(JsonTypingMessage::from).orElse(null);
final var dataMessage = envelope.data().map(JsonDataMessage::from).orElse(null);
+ final var editMessage = envelope.edit().map(JsonEditMessage::from).orElse(null);
final var storyMessage = envelope.story().map(JsonStoryMessage::from).orElse(null);
final var syncMessage = envelope.sync().map(JsonSyncMessage::from).orElse(null);
final var callMessage = envelope.call().map(JsonCallMessage::from).orElse(null);
sourceDevice,
timestamp,
dataMessage,
+ editMessage,
storyMessage,
syncMessage,
callMessage,
package org.asamk.signal.json;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import org.asamk.signal.manager.api.MessageEnvelope;
+import org.asamk.signal.manager.api.RecipientAddress;
import java.util.UUID;
@Deprecated String destination,
String destinationNumber,
String destinationUuid,
+ @JsonInclude(JsonInclude.Include.NON_NULL) JsonEditMessage editMessage,
@JsonUnwrapped JsonDataMessage dataMessage
) {
static JsonSyncDataMessage from(MessageEnvelope.Sync.Sent transcriptMessage) {
- if (transcriptMessage.destination().isPresent()) {
- final var address = transcriptMessage.destination().get();
- return new JsonSyncDataMessage(address.getLegacyIdentifier(),
- address.number().orElse(null),
- address.uuid().map(UUID::toString).orElse(null),
- transcriptMessage.message().map(JsonDataMessage::from).orElse(null));
-
- } else {
- return new JsonSyncDataMessage(null,
- null,
- null,
- transcriptMessage.message().map(JsonDataMessage::from).orElse(null));
- }
+ return new JsonSyncDataMessage(transcriptMessage.destination()
+ .map(RecipientAddress::getLegacyIdentifier)
+ .orElse(null),
+ transcriptMessage.destination().flatMap(RecipientAddress::number).orElse(null),
+ transcriptMessage.destination().flatMap(address -> address.uuid().map(UUID::toString)).orElse(null),
+ transcriptMessage.editMessage().map(JsonEditMessage::from).orElse(null),
+ transcriptMessage.message().map(JsonDataMessage::from).orElse(null));
}
}