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