]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/ReceiveMessageHandler.java
Improve output for profile key update messages
[signal-cli] / src / main / java / org / asamk / signal / ReceiveMessageHandler.java
1 package org.asamk.signal;
2
3 import org.asamk.signal.manager.Manager;
4 import org.asamk.signal.manager.api.MessageEnvelope;
5 import org.asamk.signal.manager.api.RecipientIdentifier;
6 import org.asamk.signal.manager.api.UntrustedIdentityException;
7 import org.asamk.signal.manager.groups.GroupId;
8 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
9 import org.asamk.signal.output.PlainTextWriter;
10 import org.asamk.signal.util.DateUtils;
11 import org.asamk.signal.util.Hex;
12 import org.slf4j.helpers.MessageFormatter;
13
14 import java.util.ArrayList;
15 import java.util.stream.Collectors;
16
17 public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
18
19 final Manager m;
20 final PlainTextWriter writer;
21
22 public ReceiveMessageHandler(Manager m, final PlainTextWriter writer) {
23 this.m = m;
24 this.writer = writer;
25 }
26
27 @Override
28 public void handleMessage(MessageEnvelope envelope, Throwable exception) {
29 synchronized (writer) {
30 handleMessageInternal(envelope, exception);
31 }
32 }
33
34 private void handleMessageInternal(MessageEnvelope envelope, Throwable exception) {
35 var source = envelope.sourceAddress();
36 writer.println("Envelope from: {} (device: {})",
37 source.map(this::formatContact).orElse("unknown source"),
38 envelope.sourceDevice());
39 writer.println("Timestamp: {}", DateUtils.formatTimestamp(envelope.timestamp()));
40 writer.println("Server timestamps: received: {} delivered: {}",
41 DateUtils.formatTimestamp(envelope.serverReceivedTimestamp()),
42 DateUtils.formatTimestamp(envelope.serverDeliveredTimestamp()));
43 if (envelope.isUnidentifiedSender()) {
44 writer.println("Sent by unidentified/sealed sender");
45 }
46
47 if (exception != null) {
48 if (exception instanceof UntrustedIdentityException e) {
49 writer.println(
50 "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
51 final var recipientName = e.getSender().getLegacyIdentifier();
52 writer.println(
53 "Use 'signal-cli -a {} listIdentities -n {}', verify the key and run 'signal-cli -a {} trust -v \"FINGER_PRINT\" {}' to mark it as trusted",
54 m.getSelfNumber(),
55 recipientName,
56 m.getSelfNumber(),
57 recipientName);
58 writer.println(
59 "If you don't care about security, use 'signal-cli -a {} trust -a {}' to trust it without verification",
60 m.getSelfNumber(),
61 recipientName);
62 } else {
63 writer.println("Exception: {} ({})", exception.getMessage(), exception.getClass().getSimpleName());
64 }
65 }
66
67 if (envelope.data().isPresent()) {
68 var message = envelope.data().get();
69 printDataMessage(writer, message);
70 }
71 if (envelope.sync().isPresent()) {
72 writer.println("Received a sync message");
73 var syncMessage = envelope.sync().get();
74 printSyncMessage(writer, syncMessage);
75 }
76 if (envelope.call().isPresent()) {
77 writer.println("Received a call message");
78 var callMessage = envelope.call().get();
79 printCallMessage(writer.indentedWriter(), callMessage);
80 }
81 if (envelope.receipt().isPresent()) {
82 writer.println("Received a receipt message");
83 var receiptMessage = envelope.receipt().get();
84 printReceiptMessage(writer.indentedWriter(), receiptMessage);
85 }
86 if (envelope.typing().isPresent()) {
87 writer.println("Received a typing message");
88 var typingMessage = envelope.typing().get();
89 printTypingMessage(writer.indentedWriter(), typingMessage);
90 }
91 writer.println();
92 }
93
94 private void printDataMessage(
95 PlainTextWriter writer, MessageEnvelope.Data message
96 ) {
97 writer.println("Message timestamp: {}", DateUtils.formatTimestamp(message.timestamp()));
98 if (message.isViewOnce()) {
99 writer.println("=VIEW ONCE=");
100 }
101
102 if (message.body().isPresent()) {
103 writer.println("Body: {}", message.body().get());
104 }
105 if (message.groupContext().isPresent()) {
106 writer.println("Group info:");
107 final var groupContext = message.groupContext().get();
108 printGroupContext(writer.indentedWriter(), groupContext);
109 }
110 if (message.groupCallUpdate().isPresent()) {
111 writer.println("Group call update:");
112 final var groupCallUpdate = message.groupCallUpdate().get();
113 writer.indentedWriter().println("Era id: {}", groupCallUpdate.eraId());
114 }
115 if (message.previews().size() > 0) {
116 writer.println("Previews:");
117 final var previews = message.previews();
118 for (var preview : previews) {
119 writer.println("- Preview");
120 printPreview(writer.indentedWriter(), preview);
121 }
122 }
123 if (message.sharedContacts().size() > 0) {
124 writer.println("Contacts:");
125 for (var contact : message.sharedContacts()) {
126 writer.println("- Contact:");
127 printSharedContact(writer.indentedWriter(), contact);
128 }
129 }
130 if (message.sticker().isPresent()) {
131 final var sticker = message.sticker().get();
132 writer.println("Sticker:");
133 printSticker(writer.indentedWriter(), sticker);
134 }
135 if (message.isEndSession()) {
136 writer.println("Is end session");
137 }
138 if (message.isExpirationUpdate()) {
139 writer.println("Is Expiration update: true");
140 }
141 if (message.expiresInSeconds() > 0) {
142 writer.println("Expires in: {} seconds", message.expiresInSeconds());
143 }
144 if (message.isProfileKeyUpdate()) {
145 writer.println("Profile key update");
146 }
147 if (message.hasProfileKey()) {
148 writer.println("With profile key");
149 }
150 if (message.reaction().isPresent()) {
151 writer.println("Reaction:");
152 final var reaction = message.reaction().get();
153 printReaction(writer.indentedWriter(), reaction);
154 }
155 if (message.quote().isPresent()) {
156 writer.println("Quote:");
157 var quote = message.quote().get();
158 printQuote(writer.indentedWriter(), quote);
159 }
160 if (message.remoteDeleteId().isPresent()) {
161 final var remoteDelete = message.remoteDeleteId().get();
162 writer.println("Remote delete message: timestamp = {}", remoteDelete);
163 }
164 if (message.mentions().size() > 0) {
165 writer.println("Mentions:");
166 for (var mention : message.mentions()) {
167 printMention(writer, mention);
168 }
169 }
170 if (message.attachments().size() > 0) {
171 writer.println("Attachments:");
172 for (var attachment : message.attachments()) {
173 writer.println("- Attachment:");
174 printAttachment(writer.indentedWriter(), attachment);
175 }
176 }
177 }
178
179 private void printTypingMessage(
180 final PlainTextWriter writer, final MessageEnvelope.Typing typingMessage
181 ) {
182 writer.println("Action: {}", typingMessage.type());
183 writer.println("Timestamp: {}", DateUtils.formatTimestamp(typingMessage.timestamp()));
184 if (typingMessage.groupId().isPresent()) {
185 writer.println("Group Info:");
186 final var groupId = typingMessage.groupId().get();
187 printGroupInfo(writer.indentedWriter(), groupId);
188 }
189 }
190
191 private void printReceiptMessage(
192 final PlainTextWriter writer, final MessageEnvelope.Receipt receiptMessage
193 ) {
194 writer.println("When: {}", DateUtils.formatTimestamp(receiptMessage.when()));
195 if (receiptMessage.type() == MessageEnvelope.Receipt.Type.DELIVERY) {
196 writer.println("Is delivery receipt");
197 }
198 if (receiptMessage.type() == MessageEnvelope.Receipt.Type.READ) {
199 writer.println("Is read receipt");
200 }
201 if (receiptMessage.type() == MessageEnvelope.Receipt.Type.VIEWED) {
202 writer.println("Is viewed receipt");
203 }
204 writer.println("Timestamps:");
205 for (long timestamp : receiptMessage.timestamps()) {
206 writer.println("- {}", DateUtils.formatTimestamp(timestamp));
207 }
208 }
209
210 private void printCallMessage(
211 final PlainTextWriter writer, final MessageEnvelope.Call callMessage
212 ) {
213 if (callMessage.destinationDeviceId().isPresent()) {
214 final var deviceId = callMessage.destinationDeviceId().get();
215 writer.println("Destination device id: {}", deviceId);
216 }
217 if (callMessage.groupId().isPresent()) {
218 final var groupId = callMessage.groupId().get();
219 writer.println("Destination group id: {}", groupId);
220 }
221 if (callMessage.timestamp().isPresent()) {
222 writer.println("Timestamp: {}", DateUtils.formatTimestamp(callMessage.timestamp().get()));
223 }
224 if (callMessage.answer().isPresent()) {
225 var answerMessage = callMessage.answer().get();
226 writer.println("Answer message: {}, sdp: {})", answerMessage.id(), answerMessage.sdp());
227 }
228 if (callMessage.busy().isPresent()) {
229 var busyMessage = callMessage.busy().get();
230 writer.println("Busy message: {}", busyMessage.id());
231 }
232 if (callMessage.hangup().isPresent()) {
233 var hangupMessage = callMessage.hangup().get();
234 writer.println("Hangup message: {}", hangupMessage.id());
235 }
236 if (callMessage.iceUpdate().size() > 0) {
237 writer.println("Ice update messages:");
238 var iceUpdateMessages = callMessage.iceUpdate();
239 for (var iceUpdateMessage : iceUpdateMessages) {
240 writer.println("- {}, sdp: {}", iceUpdateMessage.id(), iceUpdateMessage.sdp());
241 }
242 }
243 if (callMessage.offer().isPresent()) {
244 var offerMessage = callMessage.offer().get();
245 writer.println("Offer message: {}, sdp: {}", offerMessage.id(), offerMessage.sdp());
246 }
247 if (callMessage.opaque().isPresent()) {
248 final var opaqueMessage = callMessage.opaque().get();
249 writer.println("Opaque message: size {}, urgency: {}",
250 opaqueMessage.opaque().length,
251 opaqueMessage.urgency().name());
252 }
253 }
254
255 private void printSyncMessage(
256 final PlainTextWriter writer, final MessageEnvelope.Sync syncMessage
257 ) {
258 if (syncMessage.contacts().isPresent()) {
259 final var contactsMessage = syncMessage.contacts().get();
260 var type = contactsMessage.isComplete() ? "complete" : "partial";
261 writer.println("Received {} sync contacts:", type);
262 }
263 if (syncMessage.groups().isPresent()) {
264 writer.println("Received sync groups.");
265 }
266 if (syncMessage.read().size() > 0) {
267 writer.println("Received sync read messages list");
268 for (var rm : syncMessage.read()) {
269 writer.println("- From: {} Message timestamp: {}",
270 formatContact(rm.sender()),
271 DateUtils.formatTimestamp(rm.timestamp()));
272 }
273 }
274 if (syncMessage.viewed().size() > 0) {
275 writer.println("Received sync viewed messages list");
276 for (var vm : syncMessage.viewed()) {
277 writer.println("- From: {} Message timestamp: {}",
278 formatContact(vm.sender()),
279 DateUtils.formatTimestamp(vm.timestamp()));
280 }
281 }
282 if (syncMessage.sent().isPresent()) {
283 writer.println("Received sync sent message");
284 final var sentTranscriptMessage = syncMessage.sent().get();
285 String to;
286 if (sentTranscriptMessage.destination().isPresent()) {
287 to = formatContact(sentTranscriptMessage.destination().get());
288 } else if (sentTranscriptMessage.recipients().size() > 0) {
289 to = sentTranscriptMessage.recipients()
290 .stream()
291 .map(this::formatContact)
292 .collect(Collectors.joining(", "));
293 } else {
294 to = "<unknown>";
295 }
296 writer.indentedWriter().println("To: {}", to);
297 writer.indentedWriter()
298 .println("Timestamp: {}", DateUtils.formatTimestamp(sentTranscriptMessage.timestamp()));
299 if (sentTranscriptMessage.expirationStartTimestamp() > 0) {
300 writer.indentedWriter()
301 .println("Expiration started at: {}",
302 DateUtils.formatTimestamp(sentTranscriptMessage.expirationStartTimestamp()));
303 }
304 if (sentTranscriptMessage.message().isPresent()) {
305 var message = sentTranscriptMessage.message().get();
306 printDataMessage(writer.indentedWriter(), message);
307 }
308 }
309 if (syncMessage.blocked().isPresent()) {
310 writer.println("Received sync message with block list");
311 writer.println("Blocked:");
312 final var blockedList = syncMessage.blocked().get();
313 for (var address : blockedList.recipients()) {
314 writer.println("- {}", address.getLegacyIdentifier());
315 }
316 for (var groupId : blockedList.groupIds()) {
317 writer.println("- {}", groupId.toBase64());
318 }
319 }
320 if (syncMessage.viewOnceOpen().isPresent()) {
321 final var viewOnceOpenMessage = syncMessage.viewOnceOpen().get();
322 writer.println("Received sync message with view once open message:");
323 writer.indentedWriter().println("Sender: {}", formatContact(viewOnceOpenMessage.sender()));
324 writer.indentedWriter()
325 .println("Timestamp: {}", DateUtils.formatTimestamp(viewOnceOpenMessage.timestamp()));
326 }
327 if (syncMessage.messageRequestResponse().isPresent()) {
328 final var requestResponseMessage = syncMessage.messageRequestResponse().get();
329 writer.println("Received message request response:");
330 writer.indentedWriter().println("Type: {}", requestResponseMessage.type());
331 if (requestResponseMessage.groupId().isPresent()) {
332 writer.println("For group:");
333 printGroupInfo(writer.indentedWriter(), requestResponseMessage.groupId().get());
334 }
335 if (requestResponseMessage.person().isPresent()) {
336 writer.indentedWriter().println("For Person: {}", formatContact(requestResponseMessage.person().get()));
337 }
338 }
339 }
340
341 private void printPreview(
342 final PlainTextWriter writer, final MessageEnvelope.Data.Preview preview
343 ) {
344 writer.println("Title: {}", preview.title());
345 writer.println("Description: {}", preview.description());
346 writer.println("Date: {}", DateUtils.formatTimestamp(preview.date()));
347 writer.println("Url: {}", preview.url());
348 if (preview.image().isPresent()) {
349 writer.println("Image:");
350 printAttachment(writer.indentedWriter(), preview.image().get());
351 }
352 }
353
354 private void printSticker(
355 final PlainTextWriter writer, final MessageEnvelope.Data.Sticker sticker
356 ) {
357 writer.println("Pack id: {}", Hex.toStringCondensed(sticker.packId().serialize()));
358 writer.println("Sticker id: {}", sticker.stickerId());
359 }
360
361 private void printReaction(
362 final PlainTextWriter writer, final MessageEnvelope.Data.Reaction reaction
363 ) {
364 writer.println("Emoji: {}", reaction.emoji());
365 writer.println("Target author: {}", formatContact(reaction.targetAuthor()));
366 writer.println("Target timestamp: {}", DateUtils.formatTimestamp(reaction.targetSentTimestamp()));
367 writer.println("Is remove: {}", reaction.isRemove());
368 }
369
370 private void printQuote(
371 final PlainTextWriter writer, final MessageEnvelope.Data.Quote quote
372 ) {
373 writer.println("Id: {}", quote.id());
374 writer.println("Author: {}", formatContact(quote.author()));
375 if (quote.text().isPresent()) {
376 writer.println("Text: {}", quote.text().get());
377 }
378 if (quote.mentions() != null && quote.mentions().size() > 0) {
379 writer.println("Mentions:");
380 for (var mention : quote.mentions()) {
381 printMention(writer, mention);
382 }
383 }
384 if (quote.attachments().size() > 0) {
385 writer.println("Attachments:");
386 for (var attachment : quote.attachments()) {
387 writer.println("- Attachment:");
388 printAttachment(writer.indentedWriter(), attachment);
389 }
390 }
391 }
392
393 private void printSharedContact(final PlainTextWriter writer, final MessageEnvelope.Data.SharedContact contact) {
394 writer.println("Name:");
395 var name = contact.name();
396 writer.indent(w -> {
397 if (name.display().isPresent() && !name.display().get().isBlank()) {
398 w.println("Display name: {}", name.display().get());
399 }
400 if (name.given().isPresent() && !name.given().get().isBlank()) {
401 w.println("First name: {}", name.given().get());
402 }
403 if (name.middle().isPresent() && !name.middle().get().isBlank()) {
404 w.println("Middle name: {}", name.middle().get());
405 }
406 if (name.family().isPresent() && !name.family().get().isBlank()) {
407 w.println("Family name: {}", name.family().get());
408 }
409 if (name.prefix().isPresent() && !name.prefix().get().isBlank()) {
410 w.println("Prefix name: {}", name.prefix().get());
411 }
412 if (name.suffix().isPresent() && !name.suffix().get().isBlank()) {
413 w.println("Suffix name: {}", name.suffix().get());
414 }
415 });
416
417 if (contact.avatar().isPresent()) {
418 var avatar = contact.avatar().get();
419 writer.println("Avatar: (profile: {})", avatar.isProfile());
420 printAttachment(writer.indentedWriter(), avatar.attachment());
421 }
422
423 if (contact.organization().isPresent()) {
424 writer.println("Organisation: {}", contact.organization().get());
425 }
426
427 if (contact.phone().size() > 0) {
428 writer.println("Phone details:");
429 for (var phone : contact.phone()) {
430 writer.println("- Phone:");
431 writer.indent(w -> {
432 w.println("Number: {}", phone.value());
433 w.println("Type: {}", phone.type());
434 if (phone.label().isPresent() && !phone.label().get().isBlank()) {
435 w.println("Label: {}", phone.label().get());
436 }
437 });
438 }
439 }
440
441 if (contact.email().size() > 0) {
442 writer.println("Email details:");
443 for (var email : contact.email()) {
444 writer.println("- Email:");
445 writer.indent(w -> {
446 w.println("Address: {}", email.value());
447 w.println("Type: {}", email.type());
448 if (email.label().isPresent() && !email.label().get().isBlank()) {
449 w.println("Label: {}", email.label().get());
450 }
451 });
452 }
453 }
454
455 if (contact.address().size() > 0) {
456 writer.println("Address details:");
457 for (var address : contact.address()) {
458 writer.println("- Address:");
459 writer.indent(w -> {
460 w.println("Type: {}", address.type());
461 if (address.label().isPresent() && !address.label().get().isBlank()) {
462 w.println("Label: {}", address.label().get());
463 }
464 if (address.street().isPresent() && !address.street().get().isBlank()) {
465 w.println("Street: {}", address.street().get());
466 }
467 if (address.pobox().isPresent() && !address.pobox().get().isBlank()) {
468 w.println("Pobox: {}", address.pobox().get());
469 }
470 if (address.neighborhood().isPresent() && !address.neighborhood().get().isBlank()) {
471 w.println("Neighbourhood: {}", address.neighborhood().get());
472 }
473 if (address.city().isPresent() && !address.city().get().isBlank()) {
474 w.println("City: {}", address.city().get());
475 }
476 if (address.region().isPresent() && !address.region().get().isBlank()) {
477 w.println("Region: {}", address.region().get());
478 }
479 if (address.postcode().isPresent() && !address.postcode().get().isBlank()) {
480 w.println("Postcode: {}", address.postcode().get());
481 }
482 if (address.country().isPresent() && !address.country().get().isBlank()) {
483 w.println("Country: {}", address.country().get());
484 }
485 });
486 }
487 }
488 }
489
490 private void printGroupContext(
491 final PlainTextWriter writer, final MessageEnvelope.Data.GroupContext groupContext
492 ) {
493 printGroupInfo(writer, groupContext.groupId());
494 writer.println("Revision: {}", groupContext.revision());
495 writer.println("Type: {}", groupContext.isGroupUpdate() ? "UPDATE" : "DELIVER");
496 }
497
498 private void printGroupInfo(final PlainTextWriter writer, final GroupId groupId) {
499 writer.println("Id: {}", groupId.toBase64());
500
501 var group = m.getGroup(groupId);
502 if (group != null) {
503 writer.println("Name: {}", group.title());
504 } else {
505 writer.println("Name: <Unknown group>");
506 }
507 }
508
509 private void printMention(
510 PlainTextWriter writer, MessageEnvelope.Data.Mention mention
511 ) {
512 writer.println("- {}: {} (length: {})", formatContact(mention.recipient()), mention.start(), mention.length());
513 }
514
515 private void printAttachment(PlainTextWriter writer, MessageEnvelope.Data.Attachment attachment) {
516 writer.println("Content-Type: {}", attachment.contentType());
517 writer.println("Type: {}", attachment.id().isPresent() ? "Pointer" : "Stream");
518 if (attachment.id().isPresent()) {
519 writer.println("Id: {}", attachment.id().get());
520 }
521 if (attachment.uploadTimestamp().isPresent()) {
522 writer.println("Upload timestamp: {}", DateUtils.formatTimestamp(attachment.uploadTimestamp().get()));
523 }
524 if (attachment.caption().isPresent()) {
525 writer.println("Caption: {}", attachment.caption().get());
526 }
527 if (attachment.fileName().isPresent()) {
528 writer.println("Filename: {}", attachment.fileName().get());
529 }
530 if (attachment.size().isPresent() || attachment.preview().isPresent()) {
531 writer.println("Size: {}{}",
532 attachment.size().isPresent() ? attachment.size().get() + " bytes" : "<unavailable>",
533 attachment.preview().isPresent() ? " (Preview is available: "
534 + attachment.preview().get().length
535 + " bytes)" : "");
536 }
537 if (attachment.thumbnail().isPresent()) {
538 writer.println("Thumbnail:");
539 printAttachment(writer.indentedWriter(), attachment.thumbnail().get());
540 }
541 final var flags = new ArrayList<String>();
542 if (attachment.isVoiceNote()) {
543 flags.add("voice note");
544 }
545 if (attachment.isBorderless()) {
546 flags.add("borderless");
547 }
548 if (attachment.isGif()) {
549 flags.add("video gif");
550 }
551 if (flags.size() > 0) {
552 writer.println("Flags: {}", String.join(", ", flags));
553 }
554 if (attachment.width().isPresent() || attachment.height().isPresent()) {
555 writer.println("Dimensions: {}x{}", attachment.width().orElse(0), attachment.height().orElse(0));
556 }
557 if (attachment.file().isPresent()) {
558 var file = attachment.file().get();
559 if (file.exists()) {
560 writer.println("Stored plaintext in: {}", file);
561 }
562 }
563 }
564
565 private String formatContact(RecipientAddress address) {
566 final var number = address.getLegacyIdentifier();
567 final var name = m.getContactOrProfileName(RecipientIdentifier.Single.fromAddress(address));
568 if (name == null || name.isEmpty()) {
569 return number;
570 } else {
571 return MessageFormatter.arrayFormat("“{}” {}", new Object[]{name, number}).getMessage();
572 }
573 }
574 }