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