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