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