]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java
Add command to get an attachment (#1080)
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / api / MessageEnvelope.java
1 package org.asamk.signal.manager.api;
2
3 import org.asamk.signal.manager.groups.GroupId;
4 import org.asamk.signal.manager.groups.GroupUtils;
5 import org.asamk.signal.manager.helper.RecipientAddressResolver;
6 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
7 import org.signal.libsignal.metadata.ProtocolException;
8 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
9 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
10 import org.whispersystems.signalservice.api.messages.SignalServiceContent;
11 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
12 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
13 import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
14 import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
15 import org.whispersystems.signalservice.api.messages.SignalServicePreview;
16 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
17 import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
18 import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment;
19 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
20 import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
21 import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
22 import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
23 import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
24 import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
25 import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
26 import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
27 import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
28 import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
29 import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
30 import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
31 import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
32 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
33 import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
34 import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
35
36 import java.io.File;
37 import java.util.List;
38 import java.util.Optional;
39 import java.util.Set;
40 import java.util.stream.Collectors;
41
42 public record MessageEnvelope(
43 Optional<RecipientAddress> sourceAddress,
44 int sourceDevice,
45 long timestamp,
46 long serverReceivedTimestamp,
47 long serverDeliveredTimestamp,
48 boolean isUnidentifiedSender,
49 Optional<Receipt> receipt,
50 Optional<Typing> typing,
51 Optional<Data> data,
52 Optional<Sync> sync,
53 Optional<Call> call,
54 Optional<Story> story
55 ) {
56
57 public record Receipt(long when, Type type, List<Long> timestamps) {
58
59 static Receipt from(final SignalServiceReceiptMessage receiptMessage) {
60 return new Receipt(receiptMessage.getWhen(),
61 Type.from(receiptMessage.getType()),
62 receiptMessage.getTimestamps());
63 }
64
65 public enum Type {
66 DELIVERY,
67 READ,
68 VIEWED,
69 UNKNOWN;
70
71 static Type from(SignalServiceReceiptMessage.Type type) {
72 return switch (type) {
73 case DELIVERY -> DELIVERY;
74 case READ -> READ;
75 case VIEWED -> VIEWED;
76 case UNKNOWN -> UNKNOWN;
77 };
78 }
79 }
80 }
81
82 public record Typing(long timestamp, Type type, Optional<GroupId> groupId) {
83
84 public static Typing from(final SignalServiceTypingMessage typingMessage) {
85 return new Typing(typingMessage.getTimestamp(),
86 typingMessage.isTypingStarted() ? Type.STARTED : Type.STOPPED,
87 typingMessage.getGroupId().map(GroupId::unknownVersion));
88 }
89
90 public enum Type {
91 STARTED,
92 STOPPED,
93 }
94 }
95
96 public record Data(
97 long timestamp,
98 Optional<GroupContext> groupContext,
99 Optional<StoryContext> storyContext,
100 Optional<GroupCallUpdate> groupCallUpdate,
101 Optional<String> body,
102 int expiresInSeconds,
103 boolean isExpirationUpdate,
104 boolean isViewOnce,
105 boolean isEndSession,
106 boolean isProfileKeyUpdate,
107 boolean hasProfileKey,
108 Optional<Reaction> reaction,
109 Optional<Quote> quote,
110 Optional<Payment> payment,
111 List<Attachment> attachments,
112 Optional<Long> remoteDeleteId,
113 Optional<Sticker> sticker,
114 List<SharedContact> sharedContacts,
115 List<Mention> mentions,
116 List<Preview> previews
117 ) {
118
119 static Data from(
120 final SignalServiceDataMessage dataMessage,
121 RecipientResolver recipientResolver,
122 RecipientAddressResolver addressResolver,
123 final AttachmentFileProvider fileProvider
124 ) {
125 return new Data(dataMessage.getTimestamp(),
126 dataMessage.getGroupContext().map(GroupContext::from),
127 dataMessage.getStoryContext()
128 .map((SignalServiceDataMessage.StoryContext storyContext) -> StoryContext.from(storyContext,
129 recipientResolver,
130 addressResolver)),
131 dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
132 dataMessage.getBody(),
133 dataMessage.getExpiresInSeconds(),
134 dataMessage.isExpirationUpdate(),
135 dataMessage.isViewOnce(),
136 dataMessage.isEndSession(),
137 dataMessage.isProfileKeyUpdate(),
138 dataMessage.getProfileKey().isPresent(),
139 dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
140 dataMessage.getQuote().map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
141 dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null),
142 dataMessage.getAttachments()
143 .map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
144 .orElse(List.of()),
145 dataMessage.getRemoteDelete().map(SignalServiceDataMessage.RemoteDelete::getTargetSentTimestamp),
146 dataMessage.getSticker().map(Sticker::from),
147 dataMessage.getSharedContacts()
148 .map(a -> a.stream()
149 .map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
150 .toList())
151 .orElse(List.of()),
152 dataMessage.getMentions()
153 .map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
154 .orElse(List.of()),
155 dataMessage.getPreviews()
156 .map(a -> a.stream().map(preview -> Preview.from(preview, fileProvider)).toList())
157 .orElse(List.of()));
158 }
159
160 public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
161
162 static GroupContext from(SignalServiceGroupContext groupContext) {
163 if (groupContext.getGroupV1().isPresent()) {
164 return new GroupContext(GroupId.v1(groupContext.getGroupV1().get().getGroupId()),
165 groupContext.getGroupV1Type() == SignalServiceGroup.Type.UPDATE,
166 0);
167 } else if (groupContext.getGroupV2().isPresent()) {
168 final var groupV2 = groupContext.getGroupV2().get();
169 return new GroupContext(GroupUtils.getGroupIdV2(groupV2.getMasterKey()),
170 groupV2.hasSignedGroupChange(),
171 groupV2.getRevision());
172 } else {
173 throw new RuntimeException("Invalid group context state");
174 }
175 }
176 }
177
178 public record StoryContext(RecipientAddress author, long sentTimestamp) {
179
180 static StoryContext from(
181 SignalServiceDataMessage.StoryContext storyContext,
182 RecipientResolver recipientResolver,
183 RecipientAddressResolver addressResolver
184 ) {
185 return new StoryContext(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
186 storyContext.getAuthorServiceId())).toApiRecipientAddress(), storyContext.getSentTimestamp());
187 }
188 }
189
190 public record GroupCallUpdate(String eraId) {
191
192 static GroupCallUpdate from(SignalServiceDataMessage.GroupCallUpdate groupCallUpdate) {
193 return new GroupCallUpdate(groupCallUpdate.getEraId());
194 }
195 }
196
197 public record Reaction(
198 long targetSentTimestamp, RecipientAddress targetAuthor, String emoji, boolean isRemove
199 ) {
200
201 static Reaction from(
202 SignalServiceDataMessage.Reaction reaction,
203 RecipientResolver recipientResolver,
204 RecipientAddressResolver addressResolver
205 ) {
206 return new Reaction(reaction.getTargetSentTimestamp(),
207 addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(reaction.getTargetAuthor()))
208 .toApiRecipientAddress(),
209 reaction.getEmoji(),
210 reaction.isRemove());
211 }
212 }
213
214 public record Quote(
215 long id,
216 RecipientAddress author,
217 Optional<String> text,
218 List<Mention> mentions,
219 List<Attachment> attachments
220 ) {
221
222 static Quote from(
223 SignalServiceDataMessage.Quote quote,
224 RecipientResolver recipientResolver,
225 RecipientAddressResolver addressResolver,
226 final AttachmentFileProvider fileProvider
227 ) {
228 return new Quote(quote.getId(),
229 addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor()))
230 .toApiRecipientAddress(),
231 Optional.ofNullable(quote.getText()),
232 quote.getMentions() == null
233 ? List.of()
234 : quote.getMentions()
235 .stream()
236 .map(m -> Mention.from(m, recipientResolver, addressResolver))
237 .toList(),
238 quote.getAttachments() == null
239 ? List.of()
240 : quote.getAttachments().stream().map(a -> Attachment.from(a, fileProvider)).toList());
241 }
242 }
243
244 public record Payment(String note, byte[] receipt) {
245
246 static Payment from(SignalServiceDataMessage.Payment payment) {
247 return new Payment(payment.getPaymentNotification().get().getNote(),
248 payment.getPaymentNotification().get().getReceipt());
249 }
250 }
251
252 public record Mention(RecipientAddress recipient, int start, int length) {
253
254 static Mention from(
255 SignalServiceDataMessage.Mention mention,
256 RecipientResolver recipientResolver,
257 RecipientAddressResolver addressResolver
258 ) {
259 return new Mention(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(mention.getServiceId()))
260 .toApiRecipientAddress(), mention.getStart(), mention.getLength());
261 }
262 }
263
264 public record Attachment(
265 Optional<String> id,
266 Optional<File> file,
267 Optional<String> fileName,
268 String contentType,
269 Optional<Long> uploadTimestamp,
270 Optional<Long> size,
271 Optional<byte[]> preview,
272 Optional<Attachment> thumbnail,
273 Optional<String> caption,
274 Optional<Integer> width,
275 Optional<Integer> height,
276 boolean isVoiceNote,
277 boolean isGif,
278 boolean isBorderless
279 ) {
280
281 static Attachment from(SignalServiceAttachment attachment, AttachmentFileProvider fileProvider) {
282 if (attachment.isPointer()) {
283 final var a = attachment.asPointer();
284 final var attachmentFile = fileProvider.getFile(a);
285 return new Attachment(Optional.of(attachmentFile.getName()),
286 Optional.of(attachmentFile),
287 a.getFileName(),
288 a.getContentType(),
289 a.getUploadTimestamp() == 0 ? Optional.empty() : Optional.of(a.getUploadTimestamp()),
290 a.getSize().map(Integer::longValue),
291 a.getPreview(),
292 Optional.empty(),
293 a.getCaption().map(c -> c.isEmpty() ? null : c),
294 a.getWidth() == 0 ? Optional.empty() : Optional.of(a.getWidth()),
295 a.getHeight() == 0 ? Optional.empty() : Optional.of(a.getHeight()),
296 a.getVoiceNote(),
297 a.isGif(),
298 a.isBorderless());
299 } else {
300 final var a = attachment.asStream();
301 return new Attachment(Optional.empty(),
302 Optional.empty(),
303 a.getFileName(),
304 a.getContentType(),
305 a.getUploadTimestamp() == 0 ? Optional.empty() : Optional.of(a.getUploadTimestamp()),
306 Optional.of(a.getLength()),
307 a.getPreview(),
308 Optional.empty(),
309 a.getCaption(),
310 a.getWidth() == 0 ? Optional.empty() : Optional.of(a.getWidth()),
311 a.getHeight() == 0 ? Optional.empty() : Optional.of(a.getHeight()),
312 a.getVoiceNote(),
313 a.isGif(),
314 a.isBorderless());
315 }
316 }
317
318 static Attachment from(
319 SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider
320 ) {
321 return new Attachment(Optional.empty(),
322 Optional.empty(),
323 Optional.ofNullable(a.getFileName()),
324 a.getContentType(),
325 Optional.empty(),
326 Optional.empty(),
327 Optional.empty(),
328 a.getThumbnail() == null
329 ? Optional.empty()
330 : Optional.of(Attachment.from(a.getThumbnail(), fileProvider)),
331 Optional.empty(),
332 Optional.empty(),
333 Optional.empty(),
334 false,
335 false,
336 false);
337 }
338 }
339
340 public record Sticker(StickerPackId packId, byte[] packKey, int stickerId) {
341
342 static Sticker from(SignalServiceDataMessage.Sticker sticker) {
343 return new Sticker(StickerPackId.deserialize(sticker.getPackId()),
344 sticker.getPackKey(),
345 sticker.getStickerId());
346 }
347 }
348
349 public record SharedContact(
350 Name name,
351 Optional<Avatar> avatar,
352 List<Phone> phone,
353 List<Email> email,
354 List<Address> address,
355 Optional<String> organization
356 ) {
357
358 static SharedContact from(
359 org.whispersystems.signalservice.api.messages.shared.SharedContact sharedContact,
360 final AttachmentFileProvider fileProvider
361 ) {
362 return new SharedContact(Name.from(sharedContact.getName()),
363 sharedContact.getAvatar().map(avatar1 -> Avatar.from(avatar1, fileProvider)),
364 sharedContact.getPhone().map(p -> p.stream().map(Phone::from).toList()).orElse(List.of()),
365 sharedContact.getEmail().map(p -> p.stream().map(Email::from).toList()).orElse(List.of()),
366 sharedContact.getAddress().map(p -> p.stream().map(Address::from).toList()).orElse(List.of()),
367 sharedContact.getOrganization());
368 }
369
370 public record Name(
371 Optional<String> display,
372 Optional<String> given,
373 Optional<String> family,
374 Optional<String> prefix,
375 Optional<String> suffix,
376 Optional<String> middle
377 ) {
378
379 static Name from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Name name) {
380 return new Name(name.getDisplay(),
381 name.getGiven(),
382 name.getFamily(),
383 name.getPrefix(),
384 name.getSuffix(),
385 name.getMiddle());
386 }
387 }
388
389 public record Avatar(Attachment attachment, boolean isProfile) {
390
391 static Avatar from(
392 org.whispersystems.signalservice.api.messages.shared.SharedContact.Avatar avatar,
393 final AttachmentFileProvider fileProvider
394 ) {
395 return new Avatar(Attachment.from(avatar.getAttachment(), fileProvider), avatar.isProfile());
396 }
397 }
398
399 public record Phone(
400 String value, Type type, Optional<String> label
401 ) {
402
403 static Phone from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Phone phone) {
404 return new Phone(phone.getValue(), Type.from(phone.getType()), phone.getLabel());
405 }
406
407 public enum Type {
408 HOME,
409 WORK,
410 MOBILE,
411 CUSTOM;
412
413 static Type from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Phone.Type type) {
414 return switch (type) {
415 case HOME -> HOME;
416 case WORK -> WORK;
417 case MOBILE -> MOBILE;
418 case CUSTOM -> CUSTOM;
419 };
420 }
421 }
422 }
423
424 public record Email(
425 String value, Type type, Optional<String> label
426 ) {
427
428 static Email from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Email email) {
429 return new Email(email.getValue(), Type.from(email.getType()), email.getLabel());
430 }
431
432 public enum Type {
433 HOME,
434 WORK,
435 MOBILE,
436 CUSTOM;
437
438 static Type from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Email.Type type) {
439 return switch (type) {
440 case HOME -> HOME;
441 case WORK -> WORK;
442 case MOBILE -> MOBILE;
443 case CUSTOM -> CUSTOM;
444 };
445 }
446 }
447 }
448
449 public record Address(
450 Type type,
451 Optional<String> label,
452 Optional<String> street,
453 Optional<String> pobox,
454 Optional<String> neighborhood,
455 Optional<String> city,
456 Optional<String> region,
457 Optional<String> postcode,
458 Optional<String> country
459 ) {
460
461 static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) {
462 return new Address(Address.Type.from(address.getType()),
463 address.getLabel(),
464 address.getLabel(),
465 address.getLabel(),
466 address.getLabel(),
467 address.getLabel(),
468 address.getLabel(),
469 address.getLabel(),
470 address.getLabel());
471 }
472
473 public enum Type {
474 HOME,
475 WORK,
476 CUSTOM;
477
478 static Type from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress.Type type) {
479 return switch (type) {
480 case HOME -> HOME;
481 case WORK -> WORK;
482 case CUSTOM -> CUSTOM;
483 };
484 }
485 }
486 }
487 }
488
489 public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
490
491 static Preview from(
492 SignalServicePreview preview, final AttachmentFileProvider fileProvider
493 ) {
494 return new Preview(preview.getTitle(),
495 preview.getDescription(),
496 preview.getDate(),
497 preview.getUrl(),
498 preview.getImage().map(as -> Attachment.from(as, fileProvider)));
499 }
500 }
501 }
502
503 public record Sync(
504 Optional<Sent> sent,
505 Optional<Blocked> blocked,
506 List<Read> read,
507 List<Viewed> viewed,
508 Optional<ViewOnceOpen> viewOnceOpen,
509 Optional<Contacts> contacts,
510 Optional<Groups> groups,
511 Optional<MessageRequestResponse> messageRequestResponse
512 ) {
513
514 public static Sync from(
515 final SignalServiceSyncMessage syncMessage,
516 RecipientResolver recipientResolver,
517 RecipientAddressResolver addressResolver,
518 final AttachmentFileProvider fileProvider
519 ) {
520 return new Sync(syncMessage.getSent()
521 .map(s -> Sent.from(s, recipientResolver, addressResolver, fileProvider)),
522 syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)),
523 syncMessage.getRead()
524 .map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
525 .orElse(List.of()),
526 syncMessage.getViewed()
527 .map(r -> r.stream()
528 .map(rm -> Viewed.from(rm, recipientResolver, addressResolver))
529 .toList())
530 .orElse(List.of()),
531 syncMessage.getViewOnceOpen().map(rm -> ViewOnceOpen.from(rm, recipientResolver, addressResolver)),
532 syncMessage.getContacts().map(Contacts::from),
533 syncMessage.getGroups().map(Groups::from),
534 syncMessage.getMessageRequestResponse()
535 .map(m -> MessageRequestResponse.from(m, recipientResolver, addressResolver)));
536 }
537
538 public record Sent(
539 long timestamp,
540 long expirationStartTimestamp,
541 Optional<RecipientAddress> destination,
542 Set<RecipientAddress> recipients,
543 Optional<Data> message,
544 Optional<Story> story
545 ) {
546
547 static Sent from(
548 SentTranscriptMessage sentMessage,
549 RecipientResolver recipientResolver,
550 RecipientAddressResolver addressResolver,
551 final AttachmentFileProvider fileProvider
552 ) {
553 return new Sent(sentMessage.getTimestamp(),
554 sentMessage.getExpirationStartTimestamp(),
555 sentMessage.getDestination()
556 .map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
557 .toApiRecipientAddress()),
558 sentMessage.getRecipients()
559 .stream()
560 .map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
561 .toApiRecipientAddress())
562 .collect(Collectors.toSet()),
563 sentMessage.getDataMessage()
564 .map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)),
565 sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
566 }
567 }
568
569 public record Blocked(List<RecipientAddress> recipients, List<GroupId> groupIds) {
570
571 static Blocked from(
572 BlockedListMessage blockedListMessage,
573 RecipientResolver recipientResolver,
574 RecipientAddressResolver addressResolver
575 ) {
576 return new Blocked(blockedListMessage.getAddresses()
577 .stream()
578 .map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
579 .toApiRecipientAddress())
580 .toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
581 }
582 }
583
584 public record Read(RecipientAddress sender, long timestamp) {
585
586 static Read from(
587 ReadMessage readMessage,
588 RecipientResolver recipientResolver,
589 RecipientAddressResolver addressResolver
590 ) {
591 return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
592 .toApiRecipientAddress(), readMessage.getTimestamp());
593 }
594 }
595
596 public record Viewed(RecipientAddress sender, long timestamp) {
597
598 static Viewed from(
599 ViewedMessage readMessage,
600 RecipientResolver recipientResolver,
601 RecipientAddressResolver addressResolver
602 ) {
603 return new Viewed(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
604 .toApiRecipientAddress(), readMessage.getTimestamp());
605 }
606 }
607
608 public record ViewOnceOpen(RecipientAddress sender, long timestamp) {
609
610 static ViewOnceOpen from(
611 ViewOnceOpenMessage readMessage,
612 RecipientResolver recipientResolver,
613 RecipientAddressResolver addressResolver
614 ) {
615 return new ViewOnceOpen(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
616 readMessage.getSender())).toApiRecipientAddress(), readMessage.getTimestamp());
617 }
618 }
619
620 public record Contacts(boolean isComplete) {
621
622 static Contacts from(ContactsMessage contactsMessage) {
623 return new Contacts(contactsMessage.isComplete());
624 }
625 }
626
627 public record Groups() {
628
629 static Groups from(SignalServiceAttachment groupsMessage) {
630 return new Groups();
631 }
632 }
633
634 public record MessageRequestResponse(Type type, Optional<GroupId> groupId, Optional<RecipientAddress> person) {
635
636 static MessageRequestResponse from(
637 MessageRequestResponseMessage messageRequestResponse,
638 RecipientResolver recipientResolver,
639 RecipientAddressResolver addressResolver
640 ) {
641 return new MessageRequestResponse(Type.from(messageRequestResponse.getType()),
642 messageRequestResponse.getGroupId().map(GroupId::unknownVersion),
643 messageRequestResponse.getPerson()
644 .map(p -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(p))
645 .toApiRecipientAddress()));
646 }
647
648 public enum Type {
649 UNKNOWN,
650 ACCEPT,
651 DELETE,
652 BLOCK,
653 BLOCK_AND_DELETE,
654 UNBLOCK_AND_ACCEPT;
655
656 static Type from(MessageRequestResponseMessage.Type type) {
657 return switch (type) {
658 case UNKNOWN -> UNKNOWN;
659 case ACCEPT -> ACCEPT;
660 case DELETE -> DELETE;
661 case BLOCK -> BLOCK;
662 case BLOCK_AND_DELETE -> BLOCK_AND_DELETE;
663 case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT;
664 };
665 }
666 }
667 }
668 }
669
670 public record Call(
671 Optional<Integer> destinationDeviceId,
672 Optional<GroupId> groupId,
673 Optional<Long> timestamp,
674 Optional<Offer> offer,
675 Optional<Answer> answer,
676 Optional<Hangup> hangup,
677 Optional<Busy> busy,
678 List<IceUpdate> iceUpdate,
679 Optional<Opaque> opaque
680 ) {
681
682 public static Call from(final SignalServiceCallMessage callMessage) {
683 return new Call(callMessage.getDestinationDeviceId(),
684 callMessage.getGroupId().map(GroupId::unknownVersion),
685 callMessage.getTimestamp(),
686 callMessage.getOfferMessage().map(Offer::from),
687 callMessage.getAnswerMessage().map(Answer::from),
688 callMessage.getHangupMessage().map(Hangup::from),
689 callMessage.getBusyMessage().map(Busy::from),
690 callMessage.getIceUpdateMessages()
691 .map(m -> m.stream().map(IceUpdate::from).toList())
692 .orElse(List.of()),
693 callMessage.getOpaqueMessage().map(Opaque::from));
694 }
695
696 public record Offer(long id, String sdp, Type type, byte[] opaque) {
697
698 static Offer from(OfferMessage offerMessage) {
699 return new Offer(offerMessage.getId(),
700 offerMessage.getSdp(),
701 Type.from(offerMessage.getType()),
702 offerMessage.getOpaque());
703 }
704
705 public enum Type {
706 AUDIO_CALL,
707 VIDEO_CALL;
708
709 static Type from(OfferMessage.Type type) {
710 return switch (type) {
711 case AUDIO_CALL -> AUDIO_CALL;
712 case VIDEO_CALL -> VIDEO_CALL;
713 };
714 }
715 }
716 }
717
718 public record Answer(long id, String sdp, byte[] opaque) {
719
720 static Answer from(AnswerMessage answerMessage) {
721 return new Answer(answerMessage.getId(), answerMessage.getSdp(), answerMessage.getOpaque());
722 }
723 }
724
725 public record Busy(long id) {
726
727 static Busy from(BusyMessage busyMessage) {
728 return new Busy(busyMessage.getId());
729 }
730 }
731
732 public record Hangup(long id, Type type, int deviceId, boolean isLegacy) {
733
734 static Hangup from(HangupMessage hangupMessage) {
735 return new Hangup(hangupMessage.getId(),
736 Type.from(hangupMessage.getType()),
737 hangupMessage.getDeviceId(),
738 hangupMessage.isLegacy());
739 }
740
741 public enum Type {
742 NORMAL,
743 ACCEPTED,
744 DECLINED,
745 BUSY,
746 NEED_PERMISSION;
747
748 static Type from(HangupMessage.Type type) {
749 return switch (type) {
750 case NORMAL -> NORMAL;
751 case ACCEPTED -> ACCEPTED;
752 case DECLINED -> DECLINED;
753 case BUSY -> BUSY;
754 case NEED_PERMISSION -> NEED_PERMISSION;
755 };
756 }
757 }
758 }
759
760 public record IceUpdate(long id, String sdp, byte[] opaque) {
761
762 static IceUpdate from(IceUpdateMessage iceUpdateMessage) {
763 return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getSdp(), iceUpdateMessage.getOpaque());
764 }
765 }
766
767 public record Opaque(byte[] opaque, Urgency urgency) {
768
769 static Opaque from(OpaqueMessage opaqueMessage) {
770 return new Opaque(opaqueMessage.getOpaque(), Urgency.from(opaqueMessage.getUrgency()));
771 }
772
773 public enum Urgency {
774 DROPPABLE,
775 HANDLE_IMMEDIATELY;
776
777 static Urgency from(OpaqueMessage.Urgency urgency) {
778 return switch (urgency) {
779 case DROPPABLE -> DROPPABLE;
780 case HANDLE_IMMEDIATELY -> HANDLE_IMMEDIATELY;
781 };
782 }
783 }
784 }
785 }
786
787 public record Story(
788 boolean allowsReplies,
789 Optional<GroupId> groupId,
790 Optional<Data.Attachment> fileAttachment,
791 Optional<TextAttachment> textAttachment
792 ) {
793
794 public static Story from(
795 SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider
796 ) {
797 return new Story(storyMessage.getAllowsReplies().orElse(false),
798 storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
799 storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
800 storyMessage.getTextAttachment().map(t -> TextAttachment.from(t, fileProvider)));
801 }
802
803 public record TextAttachment(
804 Optional<String> text,
805 Optional<Style> style,
806 Optional<Color> textForegroundColor,
807 Optional<Color> textBackgroundColor,
808 Optional<Data.Preview> preview,
809 Optional<Gradient> backgroundGradient,
810 Optional<Color> backgroundColor
811 ) {
812
813 static TextAttachment from(
814 SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider
815 ) {
816 return new TextAttachment(textAttachment.getText(),
817 textAttachment.getStyle().map(Style::from),
818 textAttachment.getTextForegroundColor().map(Color::new),
819 textAttachment.getTextBackgroundColor().map(Color::new),
820 textAttachment.getPreview().map(p -> Data.Preview.from(p, fileProvider)),
821 textAttachment.getBackgroundGradient().map(Gradient::from),
822 textAttachment.getBackgroundColor().map(Color::new));
823 }
824
825 public enum Style {
826 DEFAULT,
827 REGULAR,
828 BOLD,
829 SERIF,
830 SCRIPT,
831 CONDENSED;
832
833 static Style from(SignalServiceTextAttachment.Style style) {
834 return switch (style) {
835 case DEFAULT -> DEFAULT;
836 case REGULAR -> REGULAR;
837 case BOLD -> BOLD;
838 case SERIF -> SERIF;
839 case SCRIPT -> SCRIPT;
840 case CONDENSED -> CONDENSED;
841 };
842 }
843 }
844
845 public record Gradient(
846 List<Color> colors, List<Float> positions, Optional<Integer> angle
847 ) {
848
849 static Gradient from(SignalServiceTextAttachment.Gradient gradient) {
850 return new Gradient(gradient.getColors().stream().map(Color::new).toList(),
851 gradient.getPositions(),
852 gradient.getAngle());
853 }
854 }
855 }
856 }
857
858 public static MessageEnvelope from(
859 SignalServiceEnvelope envelope,
860 SignalServiceContent content,
861 RecipientResolver recipientResolver,
862 RecipientAddressResolver addressResolver,
863 final AttachmentFileProvider fileProvider,
864 Exception exception
865 ) {
866 final var source = !envelope.isUnidentifiedSender() && envelope.hasSourceUuid()
867 ? recipientResolver.resolveRecipient(envelope.getSourceAddress())
868 : envelope.isUnidentifiedSender() && content != null
869 ? recipientResolver.resolveRecipient(content.getSender())
870 : exception instanceof ProtocolException e
871 ? recipientResolver.resolveRecipient(e.getSender())
872 : null;
873 final var sourceDevice = envelope.hasSourceDevice()
874 ? envelope.getSourceDevice()
875 : content != null
876 ? content.getSenderDevice()
877 : exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
878
879 Optional<Receipt> receipt;
880 Optional<Typing> typing;
881 Optional<Data> data;
882 Optional<Sync> sync;
883 Optional<Call> call;
884 Optional<Story> story;
885 if (content != null) {
886 receipt = content.getReceiptMessage().map(Receipt::from);
887 typing = content.getTypingMessage().map(Typing::from);
888 data = content.getDataMessage()
889 .map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider));
890 sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider));
891 call = content.getCallMessage().map(Call::from);
892 story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
893 } else {
894 receipt = envelope.isReceipt() ? Optional.of(new Receipt(envelope.getServerReceivedTimestamp(),
895 Receipt.Type.DELIVERY,
896 List.of(envelope.getTimestamp()))) : Optional.empty();
897 typing = Optional.empty();
898 data = Optional.empty();
899 sync = Optional.empty();
900 call = Optional.empty();
901 story = Optional.empty();
902 }
903
904 return new MessageEnvelope(source == null
905 ? Optional.empty()
906 : Optional.of(addressResolver.resolveRecipientAddress(source).toApiRecipientAddress()),
907 sourceDevice,
908 envelope.getTimestamp(),
909 envelope.getServerReceivedTimestamp(),
910 envelope.getServerDeliveredTimestamp(),
911 envelope.isUnidentifiedSender(),
912 receipt,
913 typing,
914 data,
915 sync,
916 call,
917 story);
918 }
919
920 public interface AttachmentFileProvider {
921
922 File getFile(SignalServiceAttachmentPointer pointer);
923 }
924 }