]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
cc9db862d47dfda9669798c53a2521f9fae01e3a
[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.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 ProfileKeyCredential profileKeyCredential = null;
93 if (r.profileKeyCredential != null) {
94 try {
95 profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
96 .decode(r.profileKeyCredential));
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, profileKeyCredential, 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 ProfileKeyCredential getProfileKeyCredential(final RecipientId recipientId) {
337 final var recipient = getRecipient(recipientId);
338 return recipient == null ? null : recipient.getProfileKeyCredential();
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 .withProfileKeyCredential(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 storeProfileKeyCredential(RecipientId recipientId, final ProfileKeyCredential profileKeyCredential) {
387 synchronized (recipients) {
388 final var recipient = recipients.get(recipientId);
389 storeRecipientLocked(recipientId,
390 Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build());
391 }
392 }
393
394 public boolean isEmpty() {
395 synchronized (recipients) {
396 return recipients.isEmpty();
397 }
398 }
399
400 private void addRecipients(final Map<RecipientId, Recipient> recipients) {
401 this.recipients.putAll(recipients);
402 }
403
404 /**
405 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
406 * Has no effect, if the address contains only a number or a uuid.
407 */
408 private RecipientId resolveRecipient(RecipientAddress address, boolean isHighTrust, boolean isSelf) {
409 final Pair<RecipientId, Optional<RecipientId>> pair;
410 synchronized (recipients) {
411 pair = resolveRecipientLocked(address, isHighTrust, isSelf);
412 }
413
414 if (pair.second().isPresent()) {
415 recipientMergeHandler.mergeRecipients(pair.first(), pair.second().get());
416 }
417 return pair.first();
418 }
419
420 private Pair<RecipientId, Optional<RecipientId>> resolveRecipientLocked(
421 RecipientAddress address, boolean isHighTrust, boolean isSelf
422 ) {
423 if (isHighTrust && !isSelf) {
424 if (selfAddressProvider.getSelfAddress().matches(address)) {
425 isHighTrust = false;
426 }
427 }
428 final var byNumber = address.number().isEmpty()
429 ? Optional.<Recipient>empty()
430 : findByNumberLocked(address.number().get());
431 final var byUuid = address.uuid().isEmpty()
432 ? Optional.<Recipient>empty()
433 : findByUuidLocked(address.uuid().get());
434
435 if (byNumber.isEmpty() && byUuid.isEmpty()) {
436 logger.debug("Got new recipient, both uuid and number are unknown");
437
438 if (isHighTrust || address.uuid().isEmpty() || address.number().isEmpty()) {
439 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
440 }
441
442 return new Pair<>(addNewRecipientLocked(new RecipientAddress(address.uuid().get())), Optional.empty());
443 }
444
445 if (!isHighTrust || address.uuid().isEmpty() || address.number().isEmpty() || byNumber.equals(byUuid)) {
446 return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
447 }
448
449 if (byNumber.isEmpty()) {
450 logger.debug("Got recipient {} existing with uuid, updating with high trust number",
451 byUuid.get().getRecipientId());
452 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
453 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
454 }
455
456 final var byNumberRecipient = byNumber.get();
457
458 if (byUuid.isEmpty()) {
459 if (byNumberRecipient.getAddress().uuid().isPresent()) {
460 logger.debug(
461 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
462 byNumberRecipient.getRecipientId());
463
464 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
465 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
466 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
467 }
468
469 logger.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
470 byNumberRecipient.getRecipientId());
471 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(), address);
472 return new Pair<>(byNumberRecipient.getRecipientId(), Optional.empty());
473 }
474
475 final var byUuidRecipient = byUuid.get();
476
477 if (byNumberRecipient.getAddress().uuid().isPresent()) {
478 logger.debug(
479 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
480 byNumberRecipient.getRecipientId(),
481 byUuidRecipient.getRecipientId());
482
483 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
484 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
485 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
486 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.empty());
487 }
488
489 logger.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
490 byNumberRecipient.getRecipientId(),
491 byUuidRecipient.getRecipientId());
492 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
493 // Create a fixed RecipientId that won't update its id after merge
494 final var toBeMergedRecipientId = new RecipientId(byNumberRecipient.getRecipientId().id(), null);
495 mergeRecipientsLocked(byUuidRecipient.getRecipientId(), toBeMergedRecipientId);
496 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.of(toBeMergedRecipientId));
497 }
498
499 private RecipientId addNewRecipientLocked(final RecipientAddress address) {
500 final var nextRecipientId = nextIdLocked();
501 logger.debug("Adding new recipient {} with address {}", nextRecipientId, address);
502 storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, address, null, null, null, null));
503 return nextRecipientId;
504 }
505
506 private void updateRecipientAddressLocked(RecipientId recipientId, final RecipientAddress address) {
507 final var recipient = recipients.get(recipientId);
508 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build());
509 }
510
511 long getActualRecipientId(long recipientId) {
512 while (recipientsMerged.containsKey(recipientId)) {
513 final var newRecipientId = recipientsMerged.get(recipientId);
514 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
515 recipientId = newRecipientId;
516 }
517 return recipientId;
518 }
519
520 private void storeRecipientLocked(final RecipientId recipientId, final Recipient recipient) {
521 final var existingRecipient = recipients.get(recipientId);
522 if (existingRecipient == null || !existingRecipient.equals(recipient)) {
523 recipients.put(recipientId, recipient);
524 saveLocked();
525 }
526 }
527
528 private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
529 final var recipient = recipients.get(recipientId);
530 final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
531 recipients.put(recipientId,
532 new Recipient(recipientId,
533 recipient.getAddress(),
534 recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
535 recipient.getProfileKey() != null
536 ? recipient.getProfileKey()
537 : toBeMergedRecipient.getProfileKey(),
538 recipient.getProfileKeyCredential() != null
539 ? recipient.getProfileKeyCredential()
540 : toBeMergedRecipient.getProfileKeyCredential(),
541 recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
542 recipients.remove(toBeMergedRecipientId);
543 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
544 saveLocked();
545 }
546
547 private Optional<Recipient> findByNumberLocked(final String number) {
548 return recipients.entrySet()
549 .stream()
550 .filter(entry -> entry.getValue().getAddress().number().isPresent() && number.equals(entry.getValue()
551 .getAddress()
552 .number()
553 .get()))
554 .findFirst()
555 .map(Map.Entry::getValue);
556 }
557
558 private Optional<Recipient> findByUuidLocked(final UUID uuid) {
559 return recipients.entrySet()
560 .stream()
561 .filter(entry -> entry.getValue().getAddress().uuid().isPresent() && uuid.equals(entry.getValue()
562 .getAddress()
563 .uuid()
564 .get()))
565 .findFirst()
566 .map(Map.Entry::getValue);
567 }
568
569 private RecipientId nextIdLocked() {
570 return new RecipientId(++this.lastId, this);
571 }
572
573 private void saveLocked() {
574 if (isBulkUpdating) {
575 return;
576 }
577 final var base64 = Base64.getEncoder();
578 var storage = new Storage(recipients.entrySet().stream().map(pair -> {
579 final var recipient = pair.getValue();
580 final var recipientContact = recipient.getContact();
581 final var contact = recipientContact == null
582 ? null
583 : new Storage.Recipient.Contact(recipientContact.getGivenName(),
584 recipientContact.getFamilyName(),
585 recipientContact.getColor(),
586 recipientContact.getMessageExpirationTime(),
587 recipientContact.isBlocked(),
588 recipientContact.isArchived(),
589 recipientContact.isProfileSharingEnabled());
590 final var recipientProfile = recipient.getProfile();
591 final var profile = recipientProfile == null
592 ? null
593 : new Storage.Recipient.Profile(recipientProfile.getLastUpdateTimestamp(),
594 recipientProfile.getGivenName(),
595 recipientProfile.getFamilyName(),
596 recipientProfile.getAbout(),
597 recipientProfile.getAboutEmoji(),
598 recipientProfile.getAvatarUrlPath(),
599 recipientProfile.getMobileCoinAddress() == null
600 ? null
601 : base64.encodeToString(recipientProfile.getMobileCoinAddress()),
602 recipientProfile.getUnidentifiedAccessMode().name(),
603 recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet()));
604 return new Storage.Recipient(pair.getKey().id(),
605 recipient.getAddress().number().orElse(null),
606 recipient.getAddress().uuid().map(UUID::toString).orElse(null),
607 recipient.getProfileKey() == null
608 ? null
609 : base64.encodeToString(recipient.getProfileKey().serialize()),
610 recipient.getProfileKeyCredential() == null
611 ? null
612 : base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
613 contact,
614 profile);
615 }).toList(), lastId);
616
617 // Write to memory first to prevent corrupting the file in case of serialization errors
618 try (var inMemoryOutput = new ByteArrayOutputStream()) {
619 objectMapper.writeValue(inMemoryOutput, storage);
620
621 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
622 try (var outputStream = new FileOutputStream(file)) {
623 input.transferTo(outputStream);
624 }
625 } catch (Exception e) {
626 logger.error("Error saving recipient store file: {}", e.getMessage());
627 }
628 }
629
630 private record Storage(List<Recipient> recipients, long lastId) {
631
632 private record Recipient(
633 long id,
634 String number,
635 String uuid,
636 String profileKey,
637 String profileKeyCredential,
638 Storage.Recipient.Contact contact,
639 Storage.Recipient.Profile profile
640 ) {
641
642 private record Contact(
643 String name,
644 String familyName,
645 String color,
646 int messageExpirationTime,
647 boolean blocked,
648 boolean archived,
649 boolean profileSharingEnabled
650 ) {}
651
652 private record Profile(
653 long lastUpdateTimestamp,
654 String givenName,
655 String familyName,
656 String about,
657 String aboutEmoji,
658 String avatarUrlPath,
659 String mobileCoinAddress,
660 String unidentifiedAccessMode,
661 Set<String> capabilities
662 ) {}
663 }
664 }
665
666 public interface RecipientMergeHandler {
667
668 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId);
669 }
670 }