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