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