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