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