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