]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
7244a96c16c60adc17eb22ef0c76a2efcc22e7b2
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / recipients / RecipientStore.java
1 package org.asamk.signal.manager.storage.recipients;
2
3 import com.fasterxml.jackson.databind.ObjectMapper;
4
5 import org.asamk.signal.manager.api.Pair;
6 import org.asamk.signal.manager.api.UnregisteredRecipientException;
7 import org.asamk.signal.manager.storage.Utils;
8 import org.asamk.signal.manager.storage.contacts.ContactsStore;
9 import org.asamk.signal.manager.storage.profiles.ProfileStore;
10 import org.signal.libsignal.zkgroup.InvalidInputException;
11 import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
12 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15 import org.whispersystems.signalservice.api.push.ACI;
16 import org.whispersystems.signalservice.api.push.ServiceId;
17 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
18 import org.whispersystems.signalservice.api.util.UuidUtil;
19
20 import java.io.ByteArrayInputStream;
21 import java.io.ByteArrayOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Base64;
29 import java.util.Collection;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Objects;
34 import java.util.Optional;
35 import java.util.Set;
36 import java.util.UUID;
37 import java.util.function.Supplier;
38 import java.util.stream.Collectors;
39
40 public class RecipientStore implements RecipientResolver, RecipientTrustedResolver, ContactsStore, ProfileStore {
41
42 private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class);
43
44 private final ObjectMapper objectMapper;
45 private final File file;
46 private final RecipientMergeHandler recipientMergeHandler;
47 private final SelfAddressProvider selfAddressProvider;
48
49 private final Map<RecipientId, Recipient> recipients;
50 private final Map<Long, Long> recipientsMerged = new HashMap<>();
51
52 private long lastId;
53 private boolean isBulkUpdating;
54
55 public static RecipientStore load(
56 File file, RecipientMergeHandler recipientMergeHandler, SelfAddressProvider selfAddressProvider
57 ) {
58 final var objectMapper = Utils.createStorageObjectMapper();
59 try (var inputStream = new FileInputStream(file)) {
60 final var storage = objectMapper.readValue(inputStream, Storage.class);
61
62 final var recipientStore = new RecipientStore(objectMapper,
63 file,
64 recipientMergeHandler,
65 selfAddressProvider,
66 new HashMap<>(),
67 storage.lastId);
68 final var recipients = storage.recipients.stream().map(r -> {
69 final var recipientId = new RecipientId(r.id, recipientStore);
70 final var address = new RecipientAddress(Optional.ofNullable(r.uuid).map(UuidUtil::parseOrThrow),
71 Optional.ofNullable(r.number));
72
73 Contact contact = null;
74 if (r.contact != null) {
75 contact = new Contact(r.contact.name,
76 r.contact.familyName,
77 r.contact.color,
78 r.contact.messageExpirationTime,
79 r.contact.blocked,
80 r.contact.archived,
81 r.contact.profileSharingEnabled);
82 }
83
84 ProfileKey profileKey = null;
85 if (r.profileKey != null) {
86 try {
87 profileKey = new ProfileKey(Base64.getDecoder().decode(r.profileKey));
88 } catch (InvalidInputException ignored) {
89 }
90 }
91
92 ExpiringProfileKeyCredential expiringProfileKeyCredential = null;
93 if (r.expiringProfileKeyCredential != null) {
94 try {
95 expiringProfileKeyCredential = new ExpiringProfileKeyCredential(Base64.getDecoder()
96 .decode(r.expiringProfileKeyCredential));
97 } catch (Throwable ignored) {
98 }
99 }
100
101 Profile profile = null;
102 if (r.profile != null) {
103 profile = new Profile(r.profile.lastUpdateTimestamp,
104 r.profile.givenName,
105 r.profile.familyName,
106 r.profile.about,
107 r.profile.aboutEmoji,
108 r.profile.avatarUrlPath,
109 r.profile.mobileCoinAddress == null
110 ? null
111 : Base64.getDecoder().decode(r.profile.mobileCoinAddress),
112 Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
113 r.profile.capabilities.stream()
114 .map(Profile.Capability::valueOfOrNull)
115 .filter(Objects::nonNull)
116 .collect(Collectors.toSet()));
117 }
118
119 return new Recipient(recipientId, address, contact, profileKey, expiringProfileKeyCredential, profile);
120 }).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
121
122 recipientStore.addRecipients(recipients);
123
124 return recipientStore;
125 } catch (FileNotFoundException e) {
126 logger.trace("Creating new recipient store.");
127 return new RecipientStore(objectMapper,
128 file,
129 recipientMergeHandler,
130 selfAddressProvider,
131 new HashMap<>(),
132 0);
133 } catch (IOException e) {
134 logger.warn("Failed to load recipient store", e);
135 throw new RuntimeException(e);
136 }
137 }
138
139 private RecipientStore(
140 final ObjectMapper objectMapper,
141 final File file,
142 final RecipientMergeHandler recipientMergeHandler,
143 final SelfAddressProvider selfAddressProvider,
144 final Map<RecipientId, Recipient> recipients,
145 final long lastId
146 ) {
147 this.objectMapper = objectMapper;
148 this.file = file;
149 this.recipientMergeHandler = recipientMergeHandler;
150 this.selfAddressProvider = selfAddressProvider;
151 this.recipients = recipients;
152 this.lastId = lastId;
153 }
154
155 public void setBulkUpdating(final boolean bulkUpdating) {
156 isBulkUpdating = bulkUpdating;
157 if (!bulkUpdating) {
158 synchronized (recipients) {
159 saveLocked();
160 }
161 }
162 }
163
164 public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
165 synchronized (recipients) {
166 return getRecipient(recipientId).getAddress();
167 }
168 }
169
170 public Recipient getRecipient(RecipientId recipientId) {
171 synchronized (recipients) {
172 return recipients.get(recipientId);
173 }
174 }
175
176 public Collection<RecipientId> getRecipientIdsWithEnabledProfileSharing() {
177 synchronized (recipients) {
178 return recipients.values().stream().filter(r -> {
179 final var contact = r.getContact();
180 return contact != null && !contact.isBlocked() && contact.isProfileSharingEnabled();
181 }).map(Recipient::getRecipientId).toList();
182 }
183 }
184
185 @Override
186 public RecipientId resolveRecipient(ServiceId serviceId) {
187 return resolveRecipient(new RecipientAddress(serviceId.uuid()), false, false);
188 }
189
190 @Override
191 public RecipientId resolveRecipient(final long recipientId) {
192 final var recipient = getRecipient(new RecipientId(recipientId, this));
193 return recipient == null ? null : recipient.getRecipientId();
194 }
195
196 @Override
197 public RecipientId resolveRecipient(final String identifier) {
198 return resolveRecipient(Utils.getRecipientAddressFromIdentifier(identifier), false, false);
199 }
200
201 public RecipientId resolveRecipient(
202 final String number, Supplier<ACI> aciSupplier
203 ) throws UnregisteredRecipientException {
204 final Optional<Recipient> byNumber;
205 synchronized (recipients) {
206 byNumber = findByNumberLocked(number);
207 }
208 if (byNumber.isEmpty() || byNumber.get().getAddress().uuid().isEmpty()) {
209 final var aci = aciSupplier.get();
210 if (aci == null) {
211 throw new UnregisteredRecipientException(new RecipientAddress(null, number));
212 }
213
214 return resolveRecipient(new RecipientAddress(aci.uuid(), number), false, false);
215 }
216 return byNumber.get().getRecipientId();
217 }
218
219 public RecipientId resolveRecipient(RecipientAddress address) {
220 return resolveRecipient(address, false, false);
221 }
222
223 @Override
224 public RecipientId resolveRecipient(final SignalServiceAddress address) {
225 return resolveRecipient(new RecipientAddress(address), false, false);
226 }
227
228 @Override
229 public RecipientId resolveSelfRecipientTrusted(RecipientAddress address) {
230 return resolveRecipient(address, true, true);
231 }
232
233 public RecipientId resolveRecipientTrusted(RecipientAddress address) {
234 return resolveRecipient(address, true, false);
235 }
236
237 @Override
238 public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
239 return resolveRecipient(new RecipientAddress(address), true, false);
240 }
241
242 public List<RecipientId> resolveRecipientsTrusted(List<RecipientAddress> addresses) {
243 final List<RecipientId> recipientIds;
244 final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
245 synchronized (recipients) {
246 recipientIds = addresses.stream().map(address -> {
247 final var pair = resolveRecipientLocked(address, true, false);
248 if (pair.second().isPresent()) {
249 toBeMerged.add(new Pair<>(pair.first(), pair.second().get()));
250 }
251 return pair.first();
252 }).toList();
253 }
254 for (var pair : toBeMerged) {
255 recipientMergeHandler.mergeRecipients(pair.first(), pair.second());
256 }
257 return recipientIds;
258 }
259
260 @Override
261 public void storeContact(RecipientId recipientId, final Contact contact) {
262 synchronized (recipients) {
263 final var recipient = recipients.get(recipientId);
264 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(contact).build());
265 }
266 }
267
268 @Override
269 public Contact getContact(RecipientId recipientId) {
270 final var recipient = getRecipient(recipientId);
271 return recipient == null ? null : recipient.getContact();
272 }
273
274 @Override
275 public List<Pair<RecipientId, Contact>> getContacts() {
276 return recipients.entrySet()
277 .stream()
278 .filter(e -> e.getValue().getContact() != null)
279 .map(e -> new Pair<>(e.getKey(), e.getValue().getContact()))
280 .toList();
281 }
282
283 public List<Recipient> getRecipients(
284 boolean onlyContacts, Optional<Boolean> blocked, Set<RecipientId> recipientIds, Optional<String> name
285 ) {
286 return recipients.values()
287 .stream()
288 .filter(r -> !onlyContacts || r.getContact() != null)
289 .filter(r -> blocked.isEmpty() || (
290 blocked.get() == (
291 r.getContact() != null && r.getContact().isBlocked()
292 )
293 ))
294 .filter(r -> recipientIds.isEmpty() || (recipientIds.contains(r.getRecipientId())))
295 .filter(r -> name.isEmpty()
296 || (r.getContact() != null && name.get().equals(r.getContact().getName()))
297 || (r.getProfile() != null && name.get().equals(r.getProfile().getDisplayName())))
298 .toList();
299 }
300
301 @Override
302 public void deleteContact(RecipientId recipientId) {
303 synchronized (recipients) {
304 final var recipient = recipients.get(recipientId);
305 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(null).build());
306 }
307 }
308
309 public void deleteRecipientData(RecipientId recipientId) {
310 synchronized (recipients) {
311 logger.debug("Deleting recipient data for {}", recipientId);
312 final var recipient = recipients.get(recipientId);
313 recipient.getAddress()
314 .uuid()
315 .ifPresent(uuid -> storeRecipientLocked(recipientId,
316 Recipient.newBuilder()
317 .withRecipientId(recipientId)
318 .withAddress(new RecipientAddress(uuid))
319 .build()));
320 }
321 }
322
323 @Override
324 public Profile getProfile(final RecipientId recipientId) {
325 final var recipient = getRecipient(recipientId);
326 return recipient == null ? null : recipient.getProfile();
327 }
328
329 @Override
330 public ProfileKey getProfileKey(final RecipientId recipientId) {
331 final var recipient = getRecipient(recipientId);
332 return recipient == null ? null : recipient.getProfileKey();
333 }
334
335 @Override
336 public ExpiringProfileKeyCredential getExpiringProfileKeyCredential(final RecipientId recipientId) {
337 final var recipient = getRecipient(recipientId);
338 return recipient == null ? null : recipient.getExpiringProfileKeyCredential();
339 }
340
341 @Override
342 public void storeProfile(RecipientId recipientId, final Profile profile) {
343 synchronized (recipients) {
344 final var recipient = recipients.get(recipientId);
345 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfile(profile).build());
346 }
347 }
348
349 @Override
350 public void storeSelfProfileKey(final RecipientId recipientId, final ProfileKey profileKey) {
351 storeProfileKey(recipientId, profileKey, false);
352 }
353
354 @Override
355 public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
356 storeProfileKey(recipientId, profileKey, true);
357 }
358
359 private void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile) {
360 synchronized (recipients) {
361 final var recipient = recipients.get(recipientId);
362 if (profileKey != null && profileKey.equals(recipient.getProfileKey()) && (
363 recipient.getProfile() == null || (
364 recipient.getProfile().getUnidentifiedAccessMode() != Profile.UnidentifiedAccessMode.UNKNOWN
365 && recipient.getProfile().getUnidentifiedAccessMode()
366 != Profile.UnidentifiedAccessMode.DISABLED
367 )
368 )) {
369 return;
370 }
371
372 final var builder = Recipient.newBuilder(recipient)
373 .withProfileKey(profileKey)
374 .withExpiringProfileKeyCredential(null);
375 if (resetProfile) {
376 builder.withProfile(recipient.getProfile() == null
377 ? null
378 : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build());
379 }
380 final var newRecipient = builder.build();
381 storeRecipientLocked(recipientId, newRecipient);
382 }
383 }
384
385 @Override
386 public void storeExpiringProfileKeyCredential(
387 RecipientId recipientId, final ExpiringProfileKeyCredential expiringProfileKeyCredential
388 ) {
389 synchronized (recipients) {
390 final var recipient = recipients.get(recipientId);
391 storeRecipientLocked(recipientId,
392 Recipient.newBuilder(recipient)
393 .withExpiringProfileKeyCredential(expiringProfileKeyCredential)
394 .build());
395 }
396 }
397
398 public boolean isEmpty() {
399 synchronized (recipients) {
400 return recipients.isEmpty();
401 }
402 }
403
404 private void addRecipients(final Map<RecipientId, Recipient> recipients) {
405 this.recipients.putAll(recipients);
406 }
407
408 /**
409 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
410 * Has no effect, if the address contains only a number or a uuid.
411 */
412 private RecipientId resolveRecipient(RecipientAddress address, boolean isHighTrust, boolean isSelf) {
413 final Pair<RecipientId, Optional<RecipientId>> pair;
414 synchronized (recipients) {
415 pair = resolveRecipientLocked(address, isHighTrust, isSelf);
416 }
417
418 if (pair.second().isPresent()) {
419 recipientMergeHandler.mergeRecipients(pair.first(), pair.second().get());
420 }
421 return pair.first();
422 }
423
424 private Pair<RecipientId, Optional<RecipientId>> resolveRecipientLocked(
425 RecipientAddress address, boolean isHighTrust, boolean isSelf
426 ) {
427 if (isHighTrust && !isSelf) {
428 if (selfAddressProvider.getSelfAddress().matches(address)) {
429 isHighTrust = false;
430 }
431 }
432 final var byNumber = address.number().isEmpty()
433 ? Optional.<Recipient>empty()
434 : findByNumberLocked(address.number().get());
435 final var byUuid = address.uuid().isEmpty()
436 ? Optional.<Recipient>empty()
437 : findByUuidLocked(address.uuid().get());
438
439 if (byNumber.isEmpty() && byUuid.isEmpty()) {
440 logger.debug("Got new recipient, both uuid and number are unknown");
441
442 if (isHighTrust || address.uuid().isEmpty() || address.number().isEmpty()) {
443 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
444 }
445
446 return new Pair<>(addNewRecipientLocked(new RecipientAddress(address.uuid().get())), Optional.empty());
447 }
448
449 if (!isHighTrust || address.uuid().isEmpty() || address.number().isEmpty() || byNumber.equals(byUuid)) {
450 return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
451 }
452
453 if (byNumber.isEmpty()) {
454 logger.debug("Got recipient {} existing with uuid, updating with high trust number",
455 byUuid.get().getRecipientId());
456 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
457 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
458 }
459
460 final var byNumberRecipient = byNumber.get();
461
462 if (byUuid.isEmpty()) {
463 if (byNumberRecipient.getAddress().uuid().isPresent()) {
464 logger.debug(
465 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
466 byNumberRecipient.getRecipientId());
467
468 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
469 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
470 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
471 }
472
473 logger.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
474 byNumberRecipient.getRecipientId());
475 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(), address);
476 return new Pair<>(byNumberRecipient.getRecipientId(), Optional.empty());
477 }
478
479 final var byUuidRecipient = byUuid.get();
480
481 if (byNumberRecipient.getAddress().uuid().isPresent()) {
482 logger.debug(
483 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
484 byNumberRecipient.getRecipientId(),
485 byUuidRecipient.getRecipientId());
486
487 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
488 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
489 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
490 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.empty());
491 }
492
493 logger.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
494 byNumberRecipient.getRecipientId(),
495 byUuidRecipient.getRecipientId());
496 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
497 // Create a fixed RecipientId that won't update its id after merge
498 final var toBeMergedRecipientId = new RecipientId(byNumberRecipient.getRecipientId().id(), null);
499 mergeRecipientsLocked(byUuidRecipient.getRecipientId(), toBeMergedRecipientId);
500 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.of(toBeMergedRecipientId));
501 }
502
503 private RecipientId addNewRecipientLocked(final RecipientAddress address) {
504 final var nextRecipientId = nextIdLocked();
505 logger.debug("Adding new recipient {} with address {}", nextRecipientId, address);
506 storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, address, null, null, null, null));
507 return nextRecipientId;
508 }
509
510 private void updateRecipientAddressLocked(RecipientId recipientId, final RecipientAddress address) {
511 final var recipient = recipients.get(recipientId);
512 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build());
513 }
514
515 long getActualRecipientId(long recipientId) {
516 while (recipientsMerged.containsKey(recipientId)) {
517 final var newRecipientId = recipientsMerged.get(recipientId);
518 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
519 recipientId = newRecipientId;
520 }
521 return recipientId;
522 }
523
524 private void storeRecipientLocked(final RecipientId recipientId, final Recipient recipient) {
525 final var existingRecipient = recipients.get(recipientId);
526 if (existingRecipient == null || !existingRecipient.equals(recipient)) {
527 recipients.put(recipientId, recipient);
528 saveLocked();
529 }
530 }
531
532 private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
533 final var recipient = recipients.get(recipientId);
534 final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
535 recipients.put(recipientId,
536 new Recipient(recipientId,
537 recipient.getAddress(),
538 recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
539 recipient.getProfileKey() != null
540 ? recipient.getProfileKey()
541 : toBeMergedRecipient.getProfileKey(),
542 recipient.getExpiringProfileKeyCredential() != null
543 ? recipient.getExpiringProfileKeyCredential()
544 : toBeMergedRecipient.getExpiringProfileKeyCredential(),
545 recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
546 recipients.remove(toBeMergedRecipientId);
547 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
548 saveLocked();
549 }
550
551 private Optional<Recipient> findByNumberLocked(final String number) {
552 return recipients.entrySet()
553 .stream()
554 .filter(entry -> entry.getValue().getAddress().number().isPresent() && number.equals(entry.getValue()
555 .getAddress()
556 .number()
557 .get()))
558 .findFirst()
559 .map(Map.Entry::getValue);
560 }
561
562 private Optional<Recipient> findByUuidLocked(final UUID uuid) {
563 return recipients.entrySet()
564 .stream()
565 .filter(entry -> entry.getValue().getAddress().uuid().isPresent() && uuid.equals(entry.getValue()
566 .getAddress()
567 .uuid()
568 .get()))
569 .findFirst()
570 .map(Map.Entry::getValue);
571 }
572
573 private RecipientId nextIdLocked() {
574 return new RecipientId(++this.lastId, this);
575 }
576
577 private void saveLocked() {
578 if (isBulkUpdating) {
579 return;
580 }
581 final var base64 = Base64.getEncoder();
582 var storage = new Storage(recipients.entrySet().stream().map(pair -> {
583 final var recipient = pair.getValue();
584 final var recipientContact = recipient.getContact();
585 final var contact = recipientContact == null
586 ? null
587 : new Storage.Recipient.Contact(recipientContact.getGivenName(),
588 recipientContact.getFamilyName(),
589 recipientContact.getColor(),
590 recipientContact.getMessageExpirationTime(),
591 recipientContact.isBlocked(),
592 recipientContact.isArchived(),
593 recipientContact.isProfileSharingEnabled());
594 final var recipientProfile = recipient.getProfile();
595 final var profile = recipientProfile == null
596 ? null
597 : new Storage.Recipient.Profile(recipientProfile.getLastUpdateTimestamp(),
598 recipientProfile.getGivenName(),
599 recipientProfile.getFamilyName(),
600 recipientProfile.getAbout(),
601 recipientProfile.getAboutEmoji(),
602 recipientProfile.getAvatarUrlPath(),
603 recipientProfile.getMobileCoinAddress() == null
604 ? null
605 : base64.encodeToString(recipientProfile.getMobileCoinAddress()),
606 recipientProfile.getUnidentifiedAccessMode().name(),
607 recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet()));
608 return new Storage.Recipient(pair.getKey().id(),
609 recipient.getAddress().number().orElse(null),
610 recipient.getAddress().uuid().map(UUID::toString).orElse(null),
611 recipient.getProfileKey() == null
612 ? null
613 : base64.encodeToString(recipient.getProfileKey().serialize()),
614 recipient.getExpiringProfileKeyCredential() == null
615 ? null
616 : base64.encodeToString(recipient.getExpiringProfileKeyCredential().serialize()),
617 contact,
618 profile);
619 }).toList(), lastId);
620
621 // Write to memory first to prevent corrupting the file in case of serialization errors
622 try (var inMemoryOutput = new ByteArrayOutputStream()) {
623 objectMapper.writeValue(inMemoryOutput, storage);
624
625 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
626 try (var outputStream = new FileOutputStream(file)) {
627 input.transferTo(outputStream);
628 }
629 } catch (Exception e) {
630 logger.error("Error saving recipient store file: {}", e.getMessage());
631 }
632 }
633
634 private record Storage(List<Recipient> recipients, long lastId) {
635
636 private record Recipient(
637 long id,
638 String number,
639 String uuid,
640 String profileKey,
641 String expiringProfileKeyCredential,
642 Storage.Recipient.Contact contact,
643 Storage.Recipient.Profile profile
644 ) {
645
646 private record Contact(
647 String name,
648 String familyName,
649 String color,
650 int messageExpirationTime,
651 boolean blocked,
652 boolean archived,
653 boolean profileSharingEnabled
654 ) {}
655
656 private record Profile(
657 long lastUpdateTimestamp,
658 String givenName,
659 String familyName,
660 String about,
661 String aboutEmoji,
662 String avatarUrlPath,
663 String mobileCoinAddress,
664 String unidentifiedAccessMode,
665 Set<String> capabilities
666 ) {}
667 }
668 }
669
670 public interface RecipientMergeHandler {
671
672 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId);
673 }
674 }