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