]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java
Refactor RecipientAddress
[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.RecipientResolver;
7 import org.signal.libsignal.metadata.ProtocolException;
8 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
9 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
10 import org.whispersystems.signalservice.api.messages.SignalServiceContent;
11 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
12 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
13 import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
14 import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
15 import org.whispersystems.signalservice.api.messages.SignalServicePreview;
16 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
17 import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
18 import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment;
19 import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
20 import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
21 import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
22 import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
23 import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
24 import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
25 import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
26 import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
27 import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
28 import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
29 import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
30 import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
31 import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
32 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
33 import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
34 import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
35
36 import java.io.File;
37 import java.util.List;
38 import java.util.Optional;
39 import java.util.Set;
40 import java.util.stream.Collectors;
41
42 public record MessageEnvelope(
43 Optional<RecipientAddress> sourceAddress,
44 int sourceDevice,
45 long timestamp,
46 long serverReceivedTimestamp,
47 long serverDeliveredTimestamp,
48 boolean isUnidentifiedSender,
49 Optional<Receipt> receipt,
50 Optional<Typing> typing,
51 Optional<Data> data,
52 Optional<Sync> sync,
53 Optional<Call> call,
54 Optional<Story> story
55 ) {
56
57 public record Receipt(long when, Type type, List<Long> timestamps) {
58
59 static Receipt from(final SignalServiceReceiptMessage receiptMessage) {
60 return new Receipt(receiptMessage.getWhen(),
61 Type.from(receiptMessage.getType()),
62 receiptMessage.getTimestamps());
63 }
64
65 public enum Type {
66 DELIVERY,
67 READ,
68 VIEWED,
69 UNKNOWN;
70
71 static Type from(SignalServiceReceiptMessage.Type type) {
72 return switch (type) {
73 case DELIVERY -> DELIVERY;
74 case READ -> READ;
75 case VIEWED -> VIEWED;
76 case UNKNOWN -> UNKNOWN;
77 };
78 }
79 }
80 }
81
82 public record Typing(long timestamp, Type type, Optional<GroupId> groupId) {
83
84 public static Typing from(final SignalServiceTypingMessage typingMessage) {
85 return new Typing(typingMessage.getTimestamp(),
86 typingMessage.isTypingStarted() ? Type.STARTED : Type.STOPPED,
87 typingMessage.getGroupId().map(GroupId::unknownVersion));
88 }
89
90 public enum Type {
91 STARTED,
92 STOPPED,
93 }
94 }
95
96 public record Data(
97 long timestamp,
98 Optional<GroupContext> groupContext,
99 Optional<StoryContext> storyContext,
100 Optional<GroupCallUpdate> groupCallUpdate,
101 Optional<String> body,
102 int expiresInSeconds,
103 boolean isExpirationUpdate,
104 boolean isViewOnce,
105 boolean isEndSession,
106 boolean isProfileKeyUpdate,
107 boolean hasProfileKey,
108 Optional<Reaction> reaction,
109 Optional<Quote> quote,
110 Optional<Payment> payment,
111 List<Attachment> attachments,
112 Optional<Long> remoteDeleteId,
113 Optional<Sticker> sticker,
114 List<SharedContact> sharedContacts,
115 List<Mention> mentions,
116 List<Preview> previews
117 ) {
118
119 static Data from(
120 final SignalServiceDataMessage dataMessage,
121 RecipientResolver recipientResolver,
122 RecipientAddressResolver addressResolver,
123 final AttachmentFileProvider fileProvider
124 ) {
125 return new Data(dataMessage.getTimestamp(),
126 dataMessage.getGroupContext().map(GroupContext::from),
127 dataMessage.getStoryContext()
128 .map((SignalServiceDataMessage.StoryContext storyContext) -> StoryContext.from(storyContext,
129 recipientResolver,
130 addressResolver)),
131 dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
132 dataMessage.getBody(),
133 dataMessage.getExpiresInSeconds(),
134 dataMessage.isExpirationUpdate(),
135 dataMessage.isViewOnce(),
136 dataMessage.isEndSession(),
137 dataMessage.isProfileKeyUpdate(),
138 dataMessage.getProfileKey().isPresent(),
139 dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
140 dataMessage.getQuote().map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
141 dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null),
142 dataMessage.getAttachments()
143 .map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
144 .orElse(List.of()),
145 dataMessage.getRemoteDelete().map(SignalServiceDataMessage.RemoteDelete::getTargetSentTimestamp),
146 dataMessage.getSticker().map(Sticker::from),
147 dataMessage.getSharedContacts()
148 .map(a -> a.stream()
149 .map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
150 .toList())
151 .orElse(List.of()),
152 dataMessage.getMentions()
153 .map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
154 .orElse(List.of()),
155 dataMessage.getPreviews()
156 .map(a -> a.stream().map(preview -> Preview.from(preview, fileProvider)).toList())
157 .orElse(List.of()));
158 }
159
160 public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
161
162 static GroupContext from(SignalServiceGroupContext groupContext) {
163 if (groupContext.getGroupV1().isPresent()) {
164 return new GroupContext(GroupId.v1(groupContext.getGroupV1().get().getGroupId()),
165 groupContext.getGroupV1Type() == SignalServiceGroup.Type.UPDATE,
166 0);
167 } else if (groupContext.getGroupV2().isPresent()) {
168 final var groupV2 = groupContext.getGroupV2().get();
169 return new GroupContext(GroupUtils.getGroupIdV2(groupV2.getMasterKey()),
170 groupV2.hasSignedGroupChange(),
171 groupV2.getRevision());
172 } else {
173 throw new RuntimeException("Invalid group context state");
174 }
175 }
176 }
177
178 public record StoryContext(RecipientAddress author, long sentTimestamp) {
179
180 static StoryContext from(
181 SignalServiceDataMessage.StoryContext storyContext,
182 RecipientResolver recipientResolver,
183 RecipientAddressResolver addressResolver
184 ) {
185 return new StoryContext(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
186 storyContext.getAuthorServiceId())).toApiRecipientAddress(), storyContext.getSentTimestamp());
187 }
188 }
189
190 public record GroupCallUpdate(String eraId) {
191
192 static GroupCallUpdate from(SignalServiceDataMessage.GroupCallUpdate groupCallUpdate) {
193 return new GroupCallUpdate(groupCallUpdate.getEraId());
194 }
195 }
196
197 public record Reaction(
198 long targetSentTimestamp, RecipientAddress targetAuthor, String emoji, boolean isRemove
199 ) {
200
201 static Reaction from(
202 SignalServiceDataMessage.Reaction reaction,
203 RecipientResolver recipientResolver,
204 RecipientAddressResolver addressResolver
205 ) {
206 return new Reaction(reaction.getTargetSentTimestamp(),
207 addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(reaction.getTargetAuthor()))
208 .toApiRecipientAddress(),
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 .toApiRecipientAddress(),
231 Optional.ofNullable(quote.getText()),
232 quote.getMentions() == null
233 ? List.of()
234 : quote.getMentions()
235 .stream()
236 .map(m -> Mention.from(m, recipientResolver, addressResolver))
237 .toList(),
238 quote.getAttachments() == null
239 ? List.of()
240 : quote.getAttachments().stream().map(a -> Attachment.from(a, fileProvider)).toList());
241 }
242 }
243
244 public record Payment(String note, byte[] receipt) {
245
246 static Payment from(SignalServiceDataMessage.Payment payment) {
247 return new Payment(payment.getPaymentNotification().get().getNote(),
248 payment.getPaymentNotification().get().getReceipt());
249 }
250 }
251
252 public record Mention(RecipientAddress recipient, int start, int length) {
253
254 static Mention from(
255 SignalServiceDataMessage.Mention mention,
256 RecipientResolver recipientResolver,
257 RecipientAddressResolver addressResolver
258 ) {
259 return new Mention(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(mention.getServiceId()))
260 .toApiRecipientAddress(), mention.getStart(), 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 .toApiRecipientAddress()),
557 sentMessage.getRecipients()
558 .stream()
559 .map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
560 .toApiRecipientAddress())
561 .collect(Collectors.toSet()),
562 sentMessage.getDataMessage()
563 .map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)),
564 sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
565 }
566 }
567
568 public record Blocked(List<RecipientAddress> recipients, List<GroupId> groupIds) {
569
570 static Blocked from(
571 BlockedListMessage blockedListMessage,
572 RecipientResolver recipientResolver,
573 RecipientAddressResolver addressResolver
574 ) {
575 return new Blocked(blockedListMessage.getAddresses()
576 .stream()
577 .map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
578 .toApiRecipientAddress())
579 .toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
580 }
581 }
582
583 public record Read(RecipientAddress sender, long timestamp) {
584
585 static Read from(
586 ReadMessage readMessage,
587 RecipientResolver recipientResolver,
588 RecipientAddressResolver addressResolver
589 ) {
590 return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
591 .toApiRecipientAddress(), readMessage.getTimestamp());
592 }
593 }
594
595 public record Viewed(RecipientAddress sender, long timestamp) {
596
597 static Viewed from(
598 ViewedMessage readMessage,
599 RecipientResolver recipientResolver,
600 RecipientAddressResolver addressResolver
601 ) {
602 return new Viewed(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
603 .toApiRecipientAddress(), readMessage.getTimestamp());
604 }
605 }
606
607 public record ViewOnceOpen(RecipientAddress sender, long timestamp) {
608
609 static ViewOnceOpen from(
610 ViewOnceOpenMessage readMessage,
611 RecipientResolver recipientResolver,
612 RecipientAddressResolver addressResolver
613 ) {
614 return new ViewOnceOpen(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
615 readMessage.getSender())).toApiRecipientAddress(), readMessage.getTimestamp());
616 }
617 }
618
619 public record Contacts(boolean isComplete) {
620
621 static Contacts from(ContactsMessage contactsMessage) {
622 return new Contacts(contactsMessage.isComplete());
623 }
624 }
625
626 public record Groups() {
627
628 static Groups from(SignalServiceAttachment groupsMessage) {
629 return new Groups();
630 }
631 }
632
633 public record MessageRequestResponse(Type type, Optional<GroupId> groupId, Optional<RecipientAddress> person) {
634
635 static MessageRequestResponse from(
636 MessageRequestResponseMessage messageRequestResponse,
637 RecipientResolver recipientResolver,
638 RecipientAddressResolver addressResolver
639 ) {
640 return new MessageRequestResponse(Type.from(messageRequestResponse.getType()),
641 messageRequestResponse.getGroupId().map(GroupId::unknownVersion),
642 messageRequestResponse.getPerson()
643 .map(p -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(p))
644 .toApiRecipientAddress()));
645 }
646
647 public enum Type {
648 UNKNOWN,
649 ACCEPT,
650 DELETE,
651 BLOCK,
652 BLOCK_AND_DELETE,
653 UNBLOCK_AND_ACCEPT;
654
655 static Type from(MessageRequestResponseMessage.Type type) {
656 return switch (type) {
657 case UNKNOWN -> UNKNOWN;
658 case ACCEPT -> ACCEPT;
659 case DELETE -> DELETE;
660 case BLOCK -> BLOCK;
661 case BLOCK_AND_DELETE -> BLOCK_AND_DELETE;
662 case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT;
663 };
664 }
665 }
666 }
667 }
668
669 public record Call(
670 Optional<Integer> destinationDeviceId,
671 Optional<GroupId> groupId,
672 Optional<Long> timestamp,
673 Optional<Offer> offer,
674 Optional<Answer> answer,
675 Optional<Hangup> hangup,
676 Optional<Busy> busy,
677 List<IceUpdate> iceUpdate,
678 Optional<Opaque> opaque
679 ) {
680
681 public static Call from(final SignalServiceCallMessage callMessage) {
682 return new Call(callMessage.getDestinationDeviceId(),
683 callMessage.getGroupId().map(GroupId::unknownVersion),
684 callMessage.getTimestamp(),
685 callMessage.getOfferMessage().map(Offer::from),
686 callMessage.getAnswerMessage().map(Answer::from),
687 callMessage.getHangupMessage().map(Hangup::from),
688 callMessage.getBusyMessage().map(Busy::from),
689 callMessage.getIceUpdateMessages()
690 .map(m -> m.stream().map(IceUpdate::from).toList())
691 .orElse(List.of()),
692 callMessage.getOpaqueMessage().map(Opaque::from));
693 }
694
695 public record Offer(long id, String sdp, Type type, byte[] opaque) {
696
697 static Offer from(OfferMessage offerMessage) {
698 return new Offer(offerMessage.getId(),
699 offerMessage.getSdp(),
700 Type.from(offerMessage.getType()),
701 offerMessage.getOpaque());
702 }
703
704 public enum Type {
705 AUDIO_CALL,
706 VIDEO_CALL;
707
708 static Type from(OfferMessage.Type type) {
709 return switch (type) {
710 case AUDIO_CALL -> AUDIO_CALL;
711 case VIDEO_CALL -> VIDEO_CALL;
712 };
713 }
714 }
715 }
716
717 public record Answer(long id, String sdp, byte[] opaque) {
718
719 static Answer from(AnswerMessage answerMessage) {
720 return new Answer(answerMessage.getId(), answerMessage.getSdp(), answerMessage.getOpaque());
721 }
722 }
723
724 public record Busy(long id) {
725
726 static Busy from(BusyMessage busyMessage) {
727 return new Busy(busyMessage.getId());
728 }
729 }
730
731 public record Hangup(long id, Type type, int deviceId, boolean isLegacy) {
732
733 static Hangup from(HangupMessage hangupMessage) {
734 return new Hangup(hangupMessage.getId(),
735 Type.from(hangupMessage.getType()),
736 hangupMessage.getDeviceId(),
737 hangupMessage.isLegacy());
738 }
739
740 public enum Type {
741 NORMAL,
742 ACCEPTED,
743 DECLINED,
744 BUSY,
745 NEED_PERMISSION;
746
747 static Type from(HangupMessage.Type type) {
748 return switch (type) {
749 case NORMAL -> NORMAL;
750 case ACCEPTED -> ACCEPTED;
751 case DECLINED -> DECLINED;
752 case BUSY -> BUSY;
753 case NEED_PERMISSION -> NEED_PERMISSION;
754 };
755 }
756 }
757 }
758
759 public record IceUpdate(long id, String sdp, byte[] opaque) {
760
761 static IceUpdate from(IceUpdateMessage iceUpdateMessage) {
762 return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getSdp(), iceUpdateMessage.getOpaque());
763 }
764 }
765
766 public record Opaque(byte[] opaque, Urgency urgency) {
767
768 static Opaque from(OpaqueMessage opaqueMessage) {
769 return new Opaque(opaqueMessage.getOpaque(), Urgency.from(opaqueMessage.getUrgency()));
770 }
771
772 public enum Urgency {
773 DROPPABLE,
774 HANDLE_IMMEDIATELY;
775
776 static Urgency from(OpaqueMessage.Urgency urgency) {
777 return switch (urgency) {
778 case DROPPABLE -> DROPPABLE;
779 case HANDLE_IMMEDIATELY -> HANDLE_IMMEDIATELY;
780 };
781 }
782 }
783 }
784 }
785
786 public record Story(
787 boolean allowsReplies,
788 Optional<GroupId> groupId,
789 Optional<Data.Attachment> fileAttachment,
790 Optional<TextAttachment> textAttachment
791 ) {
792
793 public static Story from(
794 SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider
795 ) {
796 return new Story(storyMessage.getAllowsReplies().orElse(false),
797 storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
798 storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
799 storyMessage.getTextAttachment().map(t -> TextAttachment.from(t, fileProvider)));
800 }
801
802 public record TextAttachment(
803 Optional<String> text,
804 Optional<Style> style,
805 Optional<Color> textForegroundColor,
806 Optional<Color> textBackgroundColor,
807 Optional<Data.Preview> preview,
808 Optional<Gradient> backgroundGradient,
809 Optional<Color> backgroundColor
810 ) {
811
812 static TextAttachment from(
813 SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider
814 ) {
815 return new TextAttachment(textAttachment.getText(),
816 textAttachment.getStyle().map(Style::from),
817 textAttachment.getTextForegroundColor().map(Color::new),
818 textAttachment.getTextBackgroundColor().map(Color::new),
819 textAttachment.getPreview().map(p -> Data.Preview.from(p, fileProvider)),
820 textAttachment.getBackgroundGradient().map(Gradient::from),
821 textAttachment.getBackgroundColor().map(Color::new));
822 }
823
824 public enum Style {
825 DEFAULT,
826 REGULAR,
827 BOLD,
828 SERIF,
829 SCRIPT,
830 CONDENSED;
831
832 static Style from(SignalServiceTextAttachment.Style style) {
833 return switch (style) {
834 case DEFAULT -> DEFAULT;
835 case REGULAR -> REGULAR;
836 case BOLD -> BOLD;
837 case SERIF -> SERIF;
838 case SCRIPT -> SCRIPT;
839 case CONDENSED -> CONDENSED;
840 };
841 }
842 }
843
844 public record Gradient(
845 List<Color> colors, List<Float> positions, Optional<Integer> angle
846 ) {
847
848 static Gradient from(SignalServiceTextAttachment.Gradient gradient) {
849 return new Gradient(gradient.getColors().stream().map(Color::new).toList(),
850 gradient.getPositions(),
851 gradient.getAngle());
852 }
853 }
854 }
855 }
856
857 public static MessageEnvelope from(
858 SignalServiceEnvelope envelope,
859 SignalServiceContent content,
860 RecipientResolver recipientResolver,
861 RecipientAddressResolver addressResolver,
862 final AttachmentFileProvider fileProvider,
863 Exception exception
864 ) {
865 final var source = !envelope.isUnidentifiedSender() && envelope.hasSourceUuid()
866 ? recipientResolver.resolveRecipient(envelope.getSourceAddress())
867 : envelope.isUnidentifiedSender() && content != null
868 ? recipientResolver.resolveRecipient(content.getSender())
869 : exception instanceof ProtocolException e
870 ? recipientResolver.resolveRecipient(e.getSender())
871 : null;
872 final var sourceDevice = envelope.hasSourceDevice()
873 ? envelope.getSourceDevice()
874 : content != null
875 ? content.getSenderDevice()
876 : exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
877
878 Optional<Receipt> receipt;
879 Optional<Typing> typing;
880 Optional<Data> data;
881 Optional<Sync> sync;
882 Optional<Call> call;
883 Optional<Story> story;
884 if (content != null) {
885 receipt = content.getReceiptMessage().map(Receipt::from);
886 typing = content.getTypingMessage().map(Typing::from);
887 data = content.getDataMessage()
888 .map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider));
889 sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider));
890 call = content.getCallMessage().map(Call::from);
891 story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
892 } else {
893 receipt = envelope.isReceipt() ? Optional.of(new Receipt(envelope.getServerReceivedTimestamp(),
894 Receipt.Type.DELIVERY,
895 List.of(envelope.getTimestamp()))) : Optional.empty();
896 typing = Optional.empty();
897 data = Optional.empty();
898 sync = Optional.empty();
899 call = Optional.empty();
900 story = Optional.empty();
901 }
902
903 return new MessageEnvelope(source == null
904 ? Optional.empty()
905 : Optional.of(addressResolver.resolveRecipientAddress(source).toApiRecipientAddress()),
906 sourceDevice,
907 envelope.getTimestamp(),
908 envelope.getServerReceivedTimestamp(),
909 envelope.getServerDeliveredTimestamp(),
910 envelope.isUnidentifiedSender(),
911 receipt,
912 typing,
913 data,
914 sync,
915 call,
916 story);
917 }
918
919 public interface AttachmentFileProvider {
920
921 File getFile(SignalServiceAttachmentRemoteId attachmentRemoteId);
922 }
923 }