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 synchronized (writer
) {
29 handleMessageInternal(envelope
, exception
);
33 private void handleMessageInternal(MessageEnvelope envelope
, Throwable exception
) {
34 var source
= envelope
.sourceAddress();
35 writer
.println("Envelope from: {} (device: {})",
36 source
.map(this::formatContact
).orElse("unknown source"),
37 envelope
.sourceDevice());
38 writer
.println("Timestamp: {}", DateUtils
.formatTimestamp(envelope
.timestamp()));
39 writer
.println("Server timestamps: received: {} delivered: {}",
40 DateUtils
.formatTimestamp(envelope
.serverReceivedTimestamp()),
41 DateUtils
.formatTimestamp(envelope
.serverDeliveredTimestamp()));
42 if (envelope
.isUnidentifiedSender()) {
43 writer
.println("Sent by unidentified/sealed sender");
46 if (exception
!= null) {
47 if (exception
instanceof UntrustedIdentityException e
) {
49 "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
50 final var recipientName
= e
.getSender().getLegacyIdentifier();
52 "Use 'signal-cli -a {} listIdentities -n {}', verify the key and run 'signal-cli -a {} trust -v \"FINGER_PRINT\" {}' to mark it as trusted",
58 "If you don't care about security, use 'signal-cli -a {} trust -a {}' to trust it without verification",
62 writer
.println("Exception: {} ({})", exception
.getMessage(), exception
.getClass().getSimpleName());
66 if (envelope
.data().isPresent()) {
67 var message
= envelope
.data().get();
68 printDataMessage(writer
, message
);
70 if (envelope
.sync().isPresent()) {
71 writer
.println("Received a sync message");
72 var syncMessage
= envelope
.sync().get();
73 printSyncMessage(writer
, syncMessage
);
75 if (envelope
.call().isPresent()) {
76 writer
.println("Received a call message");
77 var callMessage
= envelope
.call().get();
78 printCallMessage(writer
.indentedWriter(), callMessage
);
80 if (envelope
.receipt().isPresent()) {
81 writer
.println("Received a receipt message");
82 var receiptMessage
= envelope
.receipt().get();
83 printReceiptMessage(writer
.indentedWriter(), receiptMessage
);
85 if (envelope
.typing().isPresent()) {
86 writer
.println("Received a typing message");
87 var typingMessage
= envelope
.typing().get();
88 printTypingMessage(writer
.indentedWriter(), typingMessage
);
93 private void printDataMessage(
94 PlainTextWriter writer
, MessageEnvelope
.Data message
96 writer
.println("Message timestamp: {}", DateUtils
.formatTimestamp(message
.timestamp()));
97 if (message
.isViewOnce()) {
98 writer
.println("=VIEW ONCE=");
101 if (message
.body().isPresent()) {
102 writer
.println("Body: {}", message
.body().get());
104 if (message
.groupContext().isPresent()) {
105 writer
.println("Group info:");
106 final var groupContext
= message
.groupContext().get();
107 printGroupContext(writer
.indentedWriter(), groupContext
);
109 if (message
.groupCallUpdate().isPresent()) {
110 writer
.println("Group call update:");
111 final var groupCallUpdate
= message
.groupCallUpdate().get();
112 writer
.indentedWriter().println("Era id: {}", groupCallUpdate
.eraId());
114 if (message
.previews().size() > 0) {
115 writer
.println("Previews:");
116 final var previews
= message
.previews();
117 for (var preview
: previews
) {
118 writer
.println("- Preview");
119 printPreview(writer
.indentedWriter(), preview
);
122 if (message
.sharedContacts().size() > 0) {
123 writer
.println("Contacts:");
124 for (var contact
: message
.sharedContacts()) {
125 writer
.println("- Contact:");
126 printSharedContact(writer
.indentedWriter(), contact
);
129 if (message
.sticker().isPresent()) {
130 final var sticker
= message
.sticker().get();
131 writer
.println("Sticker:");
132 printSticker(writer
.indentedWriter(), sticker
);
134 if (message
.isEndSession()) {
135 writer
.println("Is end session");
137 if (message
.isExpirationUpdate()) {
138 writer
.println("Is Expiration update: true");
140 if (message
.expiresInSeconds() > 0) {
141 writer
.println("Expires in: {} seconds", message
.expiresInSeconds());
143 if (message
.hasProfileKey()) {
144 writer
.println("Profile key update");
146 if (message
.reaction().isPresent()) {
147 writer
.println("Reaction:");
148 final var reaction
= message
.reaction().get();
149 printReaction(writer
.indentedWriter(), reaction
);
151 if (message
.quote().isPresent()) {
152 writer
.println("Quote:");
153 var quote
= message
.quote().get();
154 printQuote(writer
.indentedWriter(), quote
);
156 if (message
.remoteDeleteId().isPresent()) {
157 final var remoteDelete
= message
.remoteDeleteId().get();
158 writer
.println("Remote delete message: timestamp = {}", remoteDelete
);
160 if (message
.mentions().size() > 0) {
161 writer
.println("Mentions:");
162 for (var mention
: message
.mentions()) {
163 printMention(writer
, mention
);
166 if (message
.attachments().size() > 0) {
167 writer
.println("Attachments:");
168 for (var attachment
: message
.attachments()) {
169 writer
.println("- Attachment:");
170 printAttachment(writer
.indentedWriter(), attachment
);
175 private void printTypingMessage(
176 final PlainTextWriter writer
, final MessageEnvelope
.Typing typingMessage
178 writer
.println("Action: {}", typingMessage
.type());
179 writer
.println("Timestamp: {}", DateUtils
.formatTimestamp(typingMessage
.timestamp()));
180 if (typingMessage
.groupId().isPresent()) {
181 writer
.println("Group Info:");
182 final var groupId
= typingMessage
.groupId().get();
183 printGroupInfo(writer
.indentedWriter(), groupId
);
187 private void printReceiptMessage(
188 final PlainTextWriter writer
, final MessageEnvelope
.Receipt receiptMessage
190 writer
.println("When: {}", DateUtils
.formatTimestamp(receiptMessage
.when()));
191 if (receiptMessage
.type() == MessageEnvelope
.Receipt
.Type
.DELIVERY
) {
192 writer
.println("Is delivery receipt");
194 if (receiptMessage
.type() == MessageEnvelope
.Receipt
.Type
.READ
) {
195 writer
.println("Is read receipt");
197 if (receiptMessage
.type() == MessageEnvelope
.Receipt
.Type
.VIEWED
) {
198 writer
.println("Is viewed receipt");
200 writer
.println("Timestamps:");
201 for (long timestamp
: receiptMessage
.timestamps()) {
202 writer
.println("- {}", DateUtils
.formatTimestamp(timestamp
));
206 private void printCallMessage(
207 final PlainTextWriter writer
, final MessageEnvelope
.Call callMessage
209 if (callMessage
.destinationDeviceId().isPresent()) {
210 final var deviceId
= callMessage
.destinationDeviceId().get();
211 writer
.println("Destination device id: {}", deviceId
);
213 if (callMessage
.groupId().isPresent()) {
214 final var groupId
= callMessage
.groupId().get();
215 writer
.println("Destination group id: {}", groupId
);
217 if (callMessage
.timestamp().isPresent()) {
218 writer
.println("Timestamp: {}", DateUtils
.formatTimestamp(callMessage
.timestamp().get()));
220 if (callMessage
.answer().isPresent()) {
221 var answerMessage
= callMessage
.answer().get();
222 writer
.println("Answer message: {}, sdp: {})", answerMessage
.id(), answerMessage
.sdp());
224 if (callMessage
.busy().isPresent()) {
225 var busyMessage
= callMessage
.busy().get();
226 writer
.println("Busy message: {}", busyMessage
.id());
228 if (callMessage
.hangup().isPresent()) {
229 var hangupMessage
= callMessage
.hangup().get();
230 writer
.println("Hangup message: {}", hangupMessage
.id());
232 if (callMessage
.iceUpdate().size() > 0) {
233 writer
.println("Ice update messages:");
234 var iceUpdateMessages
= callMessage
.iceUpdate();
235 for (var iceUpdateMessage
: iceUpdateMessages
) {
236 writer
.println("- {}, sdp: {}", iceUpdateMessage
.id(), iceUpdateMessage
.sdp());
239 if (callMessage
.offer().isPresent()) {
240 var offerMessage
= callMessage
.offer().get();
241 writer
.println("Offer message: {}, sdp: {}", offerMessage
.id(), offerMessage
.sdp());
243 if (callMessage
.opaque().isPresent()) {
244 final var opaqueMessage
= callMessage
.opaque().get();
245 writer
.println("Opaque message: size {}, urgency: {}",
246 opaqueMessage
.opaque().length
,
247 opaqueMessage
.urgency().name());
251 private void printSyncMessage(
252 final PlainTextWriter writer
, final MessageEnvelope
.Sync syncMessage
254 if (syncMessage
.contacts().isPresent()) {
255 final var contactsMessage
= syncMessage
.contacts().get();
256 var type
= contactsMessage
.isComplete() ?
"complete" : "partial";
257 writer
.println("Received {} sync contacts:", type
);
259 if (syncMessage
.groups().isPresent()) {
260 writer
.println("Received sync groups.");
262 if (syncMessage
.read().size() > 0) {
263 writer
.println("Received sync read messages list");
264 for (var rm
: syncMessage
.read()) {
265 writer
.println("- From: {} Message timestamp: {}",
266 formatContact(rm
.sender()),
267 DateUtils
.formatTimestamp(rm
.timestamp()));
270 if (syncMessage
.viewed().size() > 0) {
271 writer
.println("Received sync viewed messages list");
272 for (var vm
: syncMessage
.viewed()) {
273 writer
.println("- From: {} Message timestamp: {}",
274 formatContact(vm
.sender()),
275 DateUtils
.formatTimestamp(vm
.timestamp()));
278 if (syncMessage
.sent().isPresent()) {
279 writer
.println("Received sync sent message");
280 final var sentTranscriptMessage
= syncMessage
.sent().get();
282 if (sentTranscriptMessage
.destination().isPresent()) {
283 to = formatContact(sentTranscriptMessage
.destination().get());
284 } else if (sentTranscriptMessage
.recipients().size() > 0) {
285 to = sentTranscriptMessage
.recipients()
287 .map(this::formatContact
)
288 .collect(Collectors
.joining(", "));
292 writer
.indentedWriter().println("To: {}", to);
293 writer
.indentedWriter()
294 .println("Timestamp: {}", DateUtils
.formatTimestamp(sentTranscriptMessage
.timestamp()));
295 if (sentTranscriptMessage
.expirationStartTimestamp() > 0) {
296 writer
.indentedWriter()
297 .println("Expiration started at: {}",
298 DateUtils
.formatTimestamp(sentTranscriptMessage
.expirationStartTimestamp()));
300 var message
= sentTranscriptMessage
.message();
301 printDataMessage(writer
.indentedWriter(), message
);
303 if (syncMessage
.blocked().isPresent()) {
304 writer
.println("Received sync message with block list");
305 writer
.println("Blocked:");
306 final var blockedList
= syncMessage
.blocked().get();
307 for (var address
: blockedList
.recipients()) {
308 writer
.println("- {}", address
.getLegacyIdentifier());
310 for (var groupId
: blockedList
.groupIds()) {
311 writer
.println("- {}", groupId
);
314 if (syncMessage
.viewOnceOpen().isPresent()) {
315 final var viewOnceOpenMessage
= syncMessage
.viewOnceOpen().get();
316 writer
.println("Received sync message with view once open message:");
317 writer
.indentedWriter().println("Sender: {}", formatContact(viewOnceOpenMessage
.sender()));
318 writer
.indentedWriter()
319 .println("Timestamp: {}", DateUtils
.formatTimestamp(viewOnceOpenMessage
.timestamp()));
321 if (syncMessage
.messageRequestResponse().isPresent()) {
322 final var requestResponseMessage
= syncMessage
.messageRequestResponse().get();
323 writer
.println("Received message request response:");
324 writer
.indentedWriter().println("Type: {}", requestResponseMessage
.type());
325 if (requestResponseMessage
.groupId().isPresent()) {
326 writer
.println("For group:");
327 printGroupInfo(writer
.indentedWriter(), requestResponseMessage
.groupId().get());
329 if (requestResponseMessage
.person().isPresent()) {
330 writer
.indentedWriter().println("For Person: {}", formatContact(requestResponseMessage
.person().get()));
335 private void printPreview(
336 final PlainTextWriter writer
, final MessageEnvelope
.Data
.Preview preview
338 writer
.println("Title: {}", preview
.title());
339 writer
.println("Description: {}", preview
.description());
340 writer
.println("Date: {}", DateUtils
.formatTimestamp(preview
.date()));
341 writer
.println("Url: {}", preview
.url());
342 if (preview
.image().isPresent()) {
343 writer
.println("Image:");
344 printAttachment(writer
.indentedWriter(), preview
.image().get());
348 private void printSticker(
349 final PlainTextWriter writer
, final MessageEnvelope
.Data
.Sticker sticker
351 writer
.println("Pack id: {}", Base64
.getEncoder().encodeToString(sticker
.packId()));
352 writer
.println("Pack key: {}", Base64
.getEncoder().encodeToString(sticker
.packKey()));
353 writer
.println("Sticker id: {}", sticker
.stickerId());
356 private void printReaction(
357 final PlainTextWriter writer
, final MessageEnvelope
.Data
.Reaction reaction
359 writer
.println("Emoji: {}", reaction
.emoji());
360 writer
.println("Target author: {}", formatContact(reaction
.targetAuthor()));
361 writer
.println("Target timestamp: {}", DateUtils
.formatTimestamp(reaction
.targetSentTimestamp()));
362 writer
.println("Is remove: {}", reaction
.isRemove());
365 private void printQuote(
366 final PlainTextWriter writer
, final MessageEnvelope
.Data
.Quote quote
368 writer
.println("Id: {}", quote
.id());
369 writer
.println("Author: {}", formatContact(quote
.author()));
370 if (quote
.text().isPresent()) {
371 writer
.println("Text: {}", quote
.text().get());
373 if (quote
.mentions() != null && quote
.mentions().size() > 0) {
374 writer
.println("Mentions:");
375 for (var mention
: quote
.mentions()) {
376 printMention(writer
, mention
);
379 if (quote
.attachments().size() > 0) {
380 writer
.println("Attachments:");
381 for (var attachment
: quote
.attachments()) {
382 writer
.println("- Attachment:");
383 printAttachment(writer
.indentedWriter(), attachment
);
388 private void printSharedContact(final PlainTextWriter writer
, final MessageEnvelope
.Data
.SharedContact contact
) {
389 writer
.println("Name:");
390 var name
= contact
.name();
392 if (name
.display().isPresent() && !name
.display().get().isBlank()) {
393 w
.println("Display name: {}", name
.display().get());
395 if (name
.given().isPresent() && !name
.given().get().isBlank()) {
396 w
.println("First name: {}", name
.given().get());
398 if (name
.middle().isPresent() && !name
.middle().get().isBlank()) {
399 w
.println("Middle name: {}", name
.middle().get());
401 if (name
.family().isPresent() && !name
.family().get().isBlank()) {
402 w
.println("Family name: {}", name
.family().get());
404 if (name
.prefix().isPresent() && !name
.prefix().get().isBlank()) {
405 w
.println("Prefix name: {}", name
.prefix().get());
407 if (name
.suffix().isPresent() && !name
.suffix().get().isBlank()) {
408 w
.println("Suffix name: {}", name
.suffix().get());
412 if (contact
.avatar().isPresent()) {
413 var avatar
= contact
.avatar().get();
414 writer
.println("Avatar: (profile: {})", avatar
.isProfile());
415 printAttachment(writer
.indentedWriter(), avatar
.attachment());
418 if (contact
.organization().isPresent()) {
419 writer
.println("Organisation: {}", contact
.organization().get());
422 if (contact
.phone().size() > 0) {
423 writer
.println("Phone details:");
424 for (var phone
: contact
.phone()) {
425 writer
.println("- Phone:");
427 w
.println("Number: {}", phone
.value());
428 w
.println("Type: {}", phone
.type());
429 if (phone
.label().isPresent() && !phone
.label().get().isBlank()) {
430 w
.println("Label: {}", phone
.label().get());
436 if (contact
.email().size() > 0) {
437 writer
.println("Email details:");
438 for (var email
: contact
.email()) {
439 writer
.println("- Email:");
441 w
.println("Address: {}", email
.value());
442 w
.println("Type: {}", email
.type());
443 if (email
.label().isPresent() && !email
.label().get().isBlank()) {
444 w
.println("Label: {}", email
.label().get());
450 if (contact
.address().size() > 0) {
451 writer
.println("Address details:");
452 for (var address
: contact
.address()) {
453 writer
.println("- Address:");
455 w
.println("Type: {}", address
.type());
456 if (address
.label().isPresent() && !address
.label().get().isBlank()) {
457 w
.println("Label: {}", address
.label().get());
459 if (address
.street().isPresent() && !address
.street().get().isBlank()) {
460 w
.println("Street: {}", address
.street().get());
462 if (address
.pobox().isPresent() && !address
.pobox().get().isBlank()) {
463 w
.println("Pobox: {}", address
.pobox().get());
465 if (address
.neighborhood().isPresent() && !address
.neighborhood().get().isBlank()) {
466 w
.println("Neighbourhood: {}", address
.neighborhood().get());
468 if (address
.city().isPresent() && !address
.city().get().isBlank()) {
469 w
.println("City: {}", address
.city().get());
471 if (address
.region().isPresent() && !address
.region().get().isBlank()) {
472 w
.println("Region: {}", address
.region().get());
474 if (address
.postcode().isPresent() && !address
.postcode().get().isBlank()) {
475 w
.println("Postcode: {}", address
.postcode().get());
477 if (address
.country().isPresent() && !address
.country().get().isBlank()) {
478 w
.println("Country: {}", address
.country().get());
485 private void printGroupContext(
486 final PlainTextWriter writer
, final MessageEnvelope
.Data
.GroupContext groupContext
488 printGroupInfo(writer
, groupContext
.groupId());
489 writer
.println("Revision: {}", groupContext
.revision());
490 writer
.println("Type: {}", groupContext
.isGroupUpdate() ?
"UPDATE" : "DELIVER");
493 private void printGroupInfo(final PlainTextWriter writer
, final GroupId groupId
) {
494 writer
.println("Id: {}", groupId
.toBase64());
496 var group
= m
.getGroup(groupId
);
498 writer
.println("Name: {}", group
.title());
500 writer
.println("Name: <Unknown group>");
504 private void printMention(
505 PlainTextWriter writer
, MessageEnvelope
.Data
.Mention mention
507 writer
.println("- {}: {} (length: {})", formatContact(mention
.recipient()), mention
.start(), mention
.length());
510 private void printAttachment(PlainTextWriter writer
, MessageEnvelope
.Data
.Attachment attachment
) {
511 writer
.println("Content-Type: {}", attachment
.contentType());
512 writer
.println("Type: {}", attachment
.id().isPresent() ?
"Pointer" : "Stream");
513 if (attachment
.id().isPresent()) {
514 writer
.println("Id: {}", attachment
.id().get());
516 if (attachment
.uploadTimestamp().isPresent()) {
517 writer
.println("Upload timestamp: {}", DateUtils
.formatTimestamp(attachment
.uploadTimestamp().get()));
519 if (attachment
.caption().isPresent()) {
520 writer
.println("Caption: {}", attachment
.caption().get());
522 if (attachment
.fileName().isPresent()) {
523 writer
.println("Filename: {}", attachment
.fileName().get());
525 if (attachment
.size().isPresent() || attachment
.preview().isPresent()) {
526 writer
.println("Size: {}{}",
527 attachment
.size().isPresent() ? attachment
.size().get() + " bytes" : "<unavailable>",
528 attachment
.preview().isPresent() ?
" (Preview is available: "
529 + attachment
.preview().get().length
532 if (attachment
.thumbnail().isPresent()) {
533 writer
.println("Thumbnail:");
534 printAttachment(writer
.indentedWriter(), attachment
.thumbnail().get());
536 final var flags
= new ArrayList
<String
>();
537 if (attachment
.isVoiceNote()) {
538 flags
.add("voice note");
540 if (attachment
.isBorderless()) {
541 flags
.add("borderless");
543 if (attachment
.isGif()) {
544 flags
.add("video gif");
546 if (flags
.size() > 0) {
547 writer
.println("Flags: {}", String
.join(", ", flags
));
549 if (attachment
.width().isPresent() || attachment
.height().isPresent()) {
550 writer
.println("Dimensions: {}x{}", attachment
.width().orElse(0), attachment
.height().orElse(0));
552 if (attachment
.file().isPresent()) {
553 var file
= attachment
.file().get();
555 writer
.println("Stored plaintext in: {}", file
);
560 private String
formatContact(RecipientAddress address
) {
561 final var number
= address
.getLegacyIdentifier();
562 final var name
= m
.getContactOrProfileName(RecipientIdentifier
.Single
.fromAddress(address
));
563 if (name
== null || name
.isEmpty()) {
566 return MessageFormatter
.arrayFormat("“{}” {}", new Object
[]{name
, number
}).getMessage();