1 package org
.asamk
.signal
;
3 import org
.asamk
.signal
.manager
.Manager
;
4 import org
.asamk
.signal
.manager
.UntrustedIdentityException
;
5 import org
.asamk
.signal
.manager
.api
.MessageEnvelope
;
6 import org
.asamk
.signal
.manager
.api
.RecipientIdentifier
;
7 import org
.asamk
.signal
.manager
.groups
.GroupId
;
8 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
9 import org
.asamk
.signal
.util
.DateUtils
;
10 import org
.slf4j
.helpers
.MessageFormatter
;
12 import java
.util
.ArrayList
;
13 import java
.util
.Base64
;
14 import java
.util
.stream
.Collectors
;
16 public class ReceiveMessageHandler
implements Manager
.ReceiveMessageHandler
{
19 final PlainTextWriter writer
;
21 public ReceiveMessageHandler(Manager m
, final PlainTextWriter writer
) {
27 public void handleMessage(MessageEnvelope envelope
, Throwable exception
) {
28 var source
= envelope
.sourceAddress();
29 writer
.println("Envelope from: {} (device: {})",
30 source
.map(this::formatContact
).orElse("unknown source"),
31 envelope
.sourceDevice());
32 writer
.println("Timestamp: {}", DateUtils
.formatTimestamp(envelope
.timestamp()));
33 writer
.println("Server timestamps: received: {} delivered: {}",
34 DateUtils
.formatTimestamp(envelope
.serverReceivedTimestamp()),
35 DateUtils
.formatTimestamp(envelope
.serverDeliveredTimestamp()));
36 if (envelope
.isUnidentifiedSender()) {
37 writer
.println("Sent by unidentified/sealed sender");
40 if (exception
!= null) {
41 if (exception
instanceof UntrustedIdentityException e
) {
43 "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
44 final var recipientName
= e
.getSender().getLegacyIdentifier();
46 "Use 'signal-cli -u {} listIdentities -n {}', verify the key and run 'signal-cli -u {} trust -v \"FINGER_PRINT\" {}' to mark it as trusted",
52 "If you don't care about security, use 'signal-cli -u {} trust -a {}' to trust it without verification",
56 writer
.println("Exception: {} ({})", exception
.getMessage(), exception
.getClass().getSimpleName());
60 if (envelope
.data().isPresent()) {
61 var message
= envelope
.data().get();
62 printDataMessage(writer
, message
);
64 if (envelope
.sync().isPresent()) {
65 writer
.println("Received a sync message");
66 var syncMessage
= envelope
.sync().get();
67 printSyncMessage(writer
, syncMessage
);
69 if (envelope
.call().isPresent()) {
70 writer
.println("Received a call message");
71 var callMessage
= envelope
.call().get();
72 printCallMessage(writer
.indentedWriter(), callMessage
);
74 if (envelope
.receipt().isPresent()) {
75 writer
.println("Received a receipt message");
76 var receiptMessage
= envelope
.receipt().get();
77 printReceiptMessage(writer
.indentedWriter(), receiptMessage
);
79 if (envelope
.typing().isPresent()) {
80 writer
.println("Received a typing message");
81 var typingMessage
= envelope
.typing().get();
82 printTypingMessage(writer
.indentedWriter(), typingMessage
);
87 private void printDataMessage(
88 PlainTextWriter writer
, MessageEnvelope
.Data message
90 writer
.println("Message timestamp: {}", DateUtils
.formatTimestamp(message
.timestamp()));
91 if (message
.isViewOnce()) {
92 writer
.println("=VIEW ONCE=");
95 if (message
.body().isPresent()) {
96 writer
.println("Body: {}", message
.body().get());
98 if (message
.groupContext().isPresent()) {
99 writer
.println("Group info:");
100 final var groupContext
= message
.groupContext().get();
101 printGroupContext(writer
.indentedWriter(), groupContext
);
103 if (message
.groupCallUpdate().isPresent()) {
104 writer
.println("Group call update:");
105 final var groupCallUpdate
= message
.groupCallUpdate().get();
106 writer
.indentedWriter().println("Era id: {}", groupCallUpdate
.eraId());
108 if (message
.previews().size() > 0) {
109 writer
.println("Previews:");
110 final var previews
= message
.previews();
111 for (var preview
: previews
) {
112 writer
.println("- Preview");
113 printPreview(writer
.indentedWriter(), preview
);
116 if (message
.sharedContacts().size() > 0) {
117 writer
.println("Contacts:");
118 for (var contact
: message
.sharedContacts()) {
119 writer
.println("- Contact:");
120 printSharedContact(writer
.indentedWriter(), contact
);
123 if (message
.sticker().isPresent()) {
124 final var sticker
= message
.sticker().get();
125 writer
.println("Sticker:");
126 printSticker(writer
.indentedWriter(), sticker
);
128 if (message
.isEndSession()) {
129 writer
.println("Is end session");
131 if (message
.isExpirationUpdate()) {
132 writer
.println("Is Expiration update: true");
134 if (message
.expiresInSeconds() > 0) {
135 writer
.println("Expires in: {} seconds", message
.expiresInSeconds());
137 if (message
.hasProfileKey()) {
138 writer
.println("Profile key update");
140 if (message
.reaction().isPresent()) {
141 writer
.println("Reaction:");
142 final var reaction
= message
.reaction().get();
143 printReaction(writer
.indentedWriter(), reaction
);
145 if (message
.quote().isPresent()) {
146 writer
.println("Quote:");
147 var quote
= message
.quote().get();
148 printQuote(writer
.indentedWriter(), quote
);
150 if (message
.remoteDeleteId().isPresent()) {
151 final var remoteDelete
= message
.remoteDeleteId().get();
152 writer
.println("Remote delete message: timestamp = {}", remoteDelete
);
154 if (message
.mentions().size() > 0) {
155 writer
.println("Mentions:");
156 for (var mention
: message
.mentions()) {
157 printMention(writer
, mention
);
160 if (message
.attachments().size() > 0) {
161 writer
.println("Attachments:");
162 for (var attachment
: message
.attachments()) {
163 writer
.println("- Attachment:");
164 printAttachment(writer
.indentedWriter(), attachment
);
169 private void printTypingMessage(
170 final PlainTextWriter writer
, final MessageEnvelope
.Typing typingMessage
172 writer
.println("Action: {}", typingMessage
.type());
173 writer
.println("Timestamp: {}", DateUtils
.formatTimestamp(typingMessage
.timestamp()));
174 if (typingMessage
.groupId().isPresent()) {
175 writer
.println("Group Info:");
176 final var groupId
= typingMessage
.groupId().get();
177 printGroupInfo(writer
.indentedWriter(), groupId
);
181 private void printReceiptMessage(
182 final PlainTextWriter writer
, final MessageEnvelope
.Receipt receiptMessage
184 writer
.println("When: {}", DateUtils
.formatTimestamp(receiptMessage
.when()));
185 if (receiptMessage
.type() == MessageEnvelope
.Receipt
.Type
.DELIVERY
) {
186 writer
.println("Is delivery receipt");
188 if (receiptMessage
.type() == MessageEnvelope
.Receipt
.Type
.READ
) {
189 writer
.println("Is read receipt");
191 if (receiptMessage
.type() == MessageEnvelope
.Receipt
.Type
.VIEWED
) {
192 writer
.println("Is viewed receipt");
194 writer
.println("Timestamps:");
195 for (long timestamp
: receiptMessage
.timestamps()) {
196 writer
.println("- {}", DateUtils
.formatTimestamp(timestamp
));
200 private void printCallMessage(
201 final PlainTextWriter writer
, final MessageEnvelope
.Call callMessage
203 if (callMessage
.destinationDeviceId().isPresent()) {
204 final var deviceId
= callMessage
.destinationDeviceId().get();
205 writer
.println("Destination device id: {}", deviceId
);
207 if (callMessage
.groupId().isPresent()) {
208 final var groupId
= callMessage
.groupId().get();
209 writer
.println("Destination group id: {}", groupId
);
211 if (callMessage
.timestamp().isPresent()) {
212 writer
.println("Timestamp: {}", DateUtils
.formatTimestamp(callMessage
.timestamp().get()));
214 if (callMessage
.answer().isPresent()) {
215 var answerMessage
= callMessage
.answer().get();
216 writer
.println("Answer message: {}, sdp: {})", answerMessage
.id(), answerMessage
.sdp());
218 if (callMessage
.busy().isPresent()) {
219 var busyMessage
= callMessage
.busy().get();
220 writer
.println("Busy message: {}", busyMessage
.id());
222 if (callMessage
.hangup().isPresent()) {
223 var hangupMessage
= callMessage
.hangup().get();
224 writer
.println("Hangup message: {}", hangupMessage
.id());
226 if (callMessage
.iceUpdate().size() > 0) {
227 writer
.println("Ice update messages:");
228 var iceUpdateMessages
= callMessage
.iceUpdate();
229 for (var iceUpdateMessage
: iceUpdateMessages
) {
230 writer
.println("- {}, sdp: {}", iceUpdateMessage
.id(), iceUpdateMessage
.sdp());
233 if (callMessage
.offer().isPresent()) {
234 var offerMessage
= callMessage
.offer().get();
235 writer
.println("Offer message: {}, sdp: {}", offerMessage
.id(), offerMessage
.sdp());
237 if (callMessage
.opaque().isPresent()) {
238 final var opaqueMessage
= callMessage
.opaque().get();
239 writer
.println("Opaque message: size {}, urgency: {}",
240 opaqueMessage
.opaque().length
,
241 opaqueMessage
.urgency().name());
245 private void printSyncMessage(
246 final PlainTextWriter writer
, final MessageEnvelope
.Sync syncMessage
248 if (syncMessage
.contacts().isPresent()) {
249 final var contactsMessage
= syncMessage
.contacts().get();
250 var type
= contactsMessage
.isComplete() ?
"complete" : "partial";
251 writer
.println("Received {} sync contacts:", type
);
253 if (syncMessage
.groups().isPresent()) {
254 writer
.println("Received sync groups.");
256 if (syncMessage
.read().size() > 0) {
257 writer
.println("Received sync read messages list");
258 for (var rm
: syncMessage
.read()) {
259 writer
.println("- From: {} Message timestamp: {}",
260 formatContact(rm
.sender()),
261 DateUtils
.formatTimestamp(rm
.timestamp()));
264 if (syncMessage
.viewed().size() > 0) {
265 writer
.println("Received sync viewed messages list");
266 for (var vm
: syncMessage
.viewed()) {
267 writer
.println("- From: {} Message timestamp: {}",
268 formatContact(vm
.sender()),
269 DateUtils
.formatTimestamp(vm
.timestamp()));
272 if (syncMessage
.sent().isPresent()) {
273 writer
.println("Received sync sent message");
274 final var sentTranscriptMessage
= syncMessage
.sent().get();
276 if (sentTranscriptMessage
.destination().isPresent()) {
277 to = formatContact(sentTranscriptMessage
.destination().get());
278 } else if (sentTranscriptMessage
.recipients().size() > 0) {
279 to = sentTranscriptMessage
.recipients()
281 .map(this::formatContact
)
282 .collect(Collectors
.joining(", "));
286 writer
.indentedWriter().println("To: {}", to);
287 writer
.indentedWriter()
288 .println("Timestamp: {}", DateUtils
.formatTimestamp(sentTranscriptMessage
.timestamp()));
289 if (sentTranscriptMessage
.expirationStartTimestamp() > 0) {
290 writer
.indentedWriter()
291 .println("Expiration started at: {}",
292 DateUtils
.formatTimestamp(sentTranscriptMessage
.expirationStartTimestamp()));
294 var message
= sentTranscriptMessage
.message();
295 printDataMessage(writer
.indentedWriter(), message
);
297 if (syncMessage
.blocked().isPresent()) {
298 writer
.println("Received sync message with block list");
299 writer
.println("Blocked:");
300 final var blockedList
= syncMessage
.blocked().get();
301 for (var address
: blockedList
.recipients()) {
302 writer
.println("- {}", address
.getLegacyIdentifier());
304 for (var groupId
: blockedList
.groupIds()) {
305 writer
.println("- {}", groupId
);
308 if (syncMessage
.viewOnceOpen().isPresent()) {
309 final var viewOnceOpenMessage
= syncMessage
.viewOnceOpen().get();
310 writer
.println("Received sync message with view once open message:");
311 writer
.indentedWriter().println("Sender: {}", formatContact(viewOnceOpenMessage
.sender()));
312 writer
.indentedWriter()
313 .println("Timestamp: {}", DateUtils
.formatTimestamp(viewOnceOpenMessage
.timestamp()));
315 if (syncMessage
.messageRequestResponse().isPresent()) {
316 final var requestResponseMessage
= syncMessage
.messageRequestResponse().get();
317 writer
.println("Received message request response:");
318 writer
.indentedWriter().println("Type: {}", requestResponseMessage
.type());
319 if (requestResponseMessage
.groupId().isPresent()) {
320 writer
.println("For group:");
321 printGroupInfo(writer
.indentedWriter(), requestResponseMessage
.groupId().get());
323 if (requestResponseMessage
.person().isPresent()) {
324 writer
.indentedWriter().println("For Person: {}", formatContact(requestResponseMessage
.person().get()));
329 private void printPreview(
330 final PlainTextWriter writer
, final MessageEnvelope
.Data
.Preview preview
332 writer
.println("Title: {}", preview
.title());
333 writer
.println("Description: {}", preview
.description());
334 writer
.println("Date: {}", DateUtils
.formatTimestamp(preview
.date()));
335 writer
.println("Url: {}", preview
.url());
336 if (preview
.image().isPresent()) {
337 writer
.println("Image:");
338 printAttachment(writer
.indentedWriter(), preview
.image().get());
342 private void printSticker(
343 final PlainTextWriter writer
, final MessageEnvelope
.Data
.Sticker sticker
345 writer
.println("Pack id: {}", Base64
.getEncoder().encodeToString(sticker
.packId()));
346 writer
.println("Pack key: {}", Base64
.getEncoder().encodeToString(sticker
.packKey()));
347 writer
.println("Sticker id: {}", sticker
.stickerId());
350 private void printReaction(
351 final PlainTextWriter writer
, final MessageEnvelope
.Data
.Reaction reaction
353 writer
.println("Emoji: {}", reaction
.emoji());
354 writer
.println("Target author: {}", formatContact(reaction
.targetAuthor()));
355 writer
.println("Target timestamp: {}", DateUtils
.formatTimestamp(reaction
.targetSentTimestamp()));
356 writer
.println("Is remove: {}", reaction
.isRemove());
359 private void printQuote(
360 final PlainTextWriter writer
, final MessageEnvelope
.Data
.Quote quote
362 writer
.println("Id: {}", quote
.id());
363 writer
.println("Author: {}", formatContact(quote
.author()));
364 writer
.println("Text: {}", quote
.text());
365 if (quote
.mentions() != null && quote
.mentions().size() > 0) {
366 writer
.println("Mentions:");
367 for (var mention
: quote
.mentions()) {
368 printMention(writer
, mention
);
371 if (quote
.attachments().size() > 0) {
372 writer
.println("Attachments:");
373 for (var attachment
: quote
.attachments()) {
374 writer
.println("- Filename: {}", attachment
.fileName());
376 w
.println("Type: {}", attachment
.contentType());
377 w
.println("Thumbnail:");
378 if (attachment
.thumbnail().isPresent()) {
379 printAttachment(w
, attachment
.thumbnail().get());
386 private void printSharedContact(final PlainTextWriter writer
, final MessageEnvelope
.Data
.SharedContact contact
) {
387 writer
.println("Name:");
388 var name
= contact
.name();
390 if (name
.display().isPresent() && !name
.display().get().isBlank()) {
391 w
.println("Display name: {}", name
.display().get());
393 if (name
.given().isPresent() && !name
.given().get().isBlank()) {
394 w
.println("First name: {}", name
.given().get());
396 if (name
.middle().isPresent() && !name
.middle().get().isBlank()) {
397 w
.println("Middle name: {}", name
.middle().get());
399 if (name
.family().isPresent() && !name
.family().get().isBlank()) {
400 w
.println("Family name: {}", name
.family().get());
402 if (name
.prefix().isPresent() && !name
.prefix().get().isBlank()) {
403 w
.println("Prefix name: {}", name
.prefix().get());
405 if (name
.suffix().isPresent() && !name
.suffix().get().isBlank()) {
406 w
.println("Suffix name: {}", name
.suffix().get());
410 if (contact
.avatar().isPresent()) {
411 var avatar
= contact
.avatar().get();
412 writer
.println("Avatar: (profile: {})", avatar
.isProfile());
413 printAttachment(writer
.indentedWriter(), avatar
.attachment());
416 if (contact
.organization().isPresent()) {
417 writer
.println("Organisation: {}", contact
.organization().get());
420 if (contact
.phone().size() > 0) {
421 writer
.println("Phone details:");
422 for (var phone
: contact
.phone()) {
423 writer
.println("- Phone:");
425 w
.println("Number: {}", phone
.value());
426 w
.println("Type: {}", phone
.type());
427 if (phone
.label().isPresent() && !phone
.label().get().isBlank()) {
428 w
.println("Label: {}", phone
.label().get());
434 if (contact
.email().size() > 0) {
435 writer
.println("Email details:");
436 for (var email
: contact
.email()) {
437 writer
.println("- Email:");
439 w
.println("Address: {}", email
.value());
440 w
.println("Type: {}", email
.type());
441 if (email
.label().isPresent() && !email
.label().get().isBlank()) {
442 w
.println("Label: {}", email
.label().get());
448 if (contact
.address().size() > 0) {
449 writer
.println("Address details:");
450 for (var address
: contact
.address()) {
451 writer
.println("- Address:");
453 w
.println("Type: {}", address
.type());
454 if (address
.label().isPresent() && !address
.label().get().isBlank()) {
455 w
.println("Label: {}", address
.label().get());
457 if (address
.street().isPresent() && !address
.street().get().isBlank()) {
458 w
.println("Street: {}", address
.street().get());
460 if (address
.pobox().isPresent() && !address
.pobox().get().isBlank()) {
461 w
.println("Pobox: {}", address
.pobox().get());
463 if (address
.neighborhood().isPresent() && !address
.neighborhood().get().isBlank()) {
464 w
.println("Neighbourhood: {}", address
.neighborhood().get());
466 if (address
.city().isPresent() && !address
.city().get().isBlank()) {
467 w
.println("City: {}", address
.city().get());
469 if (address
.region().isPresent() && !address
.region().get().isBlank()) {
470 w
.println("Region: {}", address
.region().get());
472 if (address
.postcode().isPresent() && !address
.postcode().get().isBlank()) {
473 w
.println("Postcode: {}", address
.postcode().get());
475 if (address
.country().isPresent() && !address
.country().get().isBlank()) {
476 w
.println("Country: {}", address
.country().get());
483 private void printGroupContext(
484 final PlainTextWriter writer
, final MessageEnvelope
.Data
.GroupContext groupContext
486 printGroupInfo(writer
, groupContext
.groupId());
487 writer
.println("Revision: {}", groupContext
.revision());
488 writer
.println("Type: {}", groupContext
.isGroupUpdate() ?
"UPDATE" : "DELIVER");
491 private void printGroupInfo(final PlainTextWriter writer
, final GroupId groupId
) {
492 writer
.println("Id: {}", groupId
.toBase64());
494 var group
= m
.getGroup(groupId
);
496 writer
.println("Name: {}", group
.title());
498 writer
.println("Name: <Unknown group>");
502 private void printMention(
503 PlainTextWriter writer
, MessageEnvelope
.Data
.Mention mention
505 writer
.println("- {}: {} (length: {})", formatContact(mention
.recipient()), mention
.start(), mention
.length());
508 private void printAttachment(PlainTextWriter writer
, MessageEnvelope
.Data
.Attachment attachment
) {
509 writer
.println("Content-Type: {}", attachment
.contentType());
510 writer
.println("Type: {}", attachment
.id().isPresent() ?
"Pointer" : "Stream");
511 if (attachment
.id().isPresent()) {
512 writer
.println("Id: {}", attachment
.id().get());
514 if (attachment
.uploadTimestamp().isPresent()) {
515 writer
.println("Upload timestamp: {}", DateUtils
.formatTimestamp(attachment
.uploadTimestamp().get()));
517 if (attachment
.caption().isPresent()) {
518 writer
.println("Caption: {}", attachment
.caption().get());
520 if (attachment
.fileName().isPresent()) {
521 writer
.println("Filename: {}", attachment
.fileName().get());
523 if (attachment
.size().isPresent() || attachment
.preview().isPresent()) {
524 writer
.println("Size: {}{}",
525 attachment
.size().isPresent() ? attachment
.size().get() + " bytes" : "<unavailable>",
526 attachment
.preview().isPresent() ?
" (Preview is available: "
527 + attachment
.preview().get().length
530 final var flags
= new ArrayList
<String
>();
531 if (attachment
.isVoiceNote()) {
532 flags
.add("voice note");
534 if (attachment
.isBorderless()) {
535 flags
.add("borderless");
537 if (attachment
.isGif()) {
538 flags
.add("video gif");
540 if (flags
.size() > 0) {
541 writer
.println("Flags: {}", String
.join(", ", flags
));
543 if (attachment
.width().isPresent() || attachment
.height().isPresent()) {
544 writer
.println("Dimensions: {}x{}", attachment
.width().orElse(0), attachment
.height().orElse(0));
546 if (attachment
.id().isPresent()) {
547 var file
= m
.getAttachmentFile(attachment
.id().get());
549 writer
.println("Stored plaintext in: {}", file
);
554 private String
formatContact(RecipientAddress address
) {
555 final var number
= address
.getLegacyIdentifier();
556 final var name
= m
.getContactOrProfileName(RecipientIdentifier
.Single
.fromAddress(address
));
557 if (name
== null || name
.isEmpty()) {
560 return MessageFormatter
.arrayFormat("“{}” {}", new Object
[]{name
, number
}).getMessage();