]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
0297f6d7d290b93ae29529f14a0a1a95e1ee6c67
[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, 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 public RecipientId resolveSelfRecipientTrusted(RecipientAddress address) {
228 return resolveRecipient(address, true, true);
229 }
230
231 public RecipientId resolveRecipientTrusted(RecipientAddress address) {
232 return resolveRecipient(address, true, false);
233 }
234
235 public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
236 return resolveRecipient(new RecipientAddress(address), true, false);
237 }
238
239 public List<RecipientId> resolveRecipientsTrusted(List<RecipientAddress> addresses) {
240 final List<RecipientId> recipientIds;
241 final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
242 synchronized (recipients) {
243 recipientIds = addresses.stream().map(address -> {
244 final var pair = resolveRecipientLocked(address, true, false);
245 if (pair.second().isPresent()) {
246 toBeMerged.add(new Pair<>(pair.first(), pair.second().get()));
247 }
248 return pair.first();
249 }).toList();
250 }
251 for (var pair : toBeMerged) {
252 recipientMergeHandler.mergeRecipients(pair.first(), pair.second());
253 }
254 return recipientIds;
255 }
256
257 @Override
258 public void storeContact(RecipientId recipientId, final Contact contact) {
259 synchronized (recipients) {
260 final var recipient = recipients.get(recipientId);
261 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(contact).build());
262 }
263 }
264
265 @Override
266 public Contact getContact(RecipientId recipientId) {
267 final var recipient = getRecipient(recipientId);
268 return recipient == null ? null : recipient.getContact();
269 }
270
271 @Override
272 public List<Pair<RecipientId, Contact>> getContacts() {
273 return recipients.entrySet()
274 .stream()
275 .filter(e -> e.getValue().getContact() != null)
276 .map(e -> new Pair<>(e.getKey(), e.getValue().getContact()))
277 .toList();
278 }
279
280 public List<Recipient> getRecipients(
281 boolean onlyContacts, Optional<Boolean> blocked, Set<RecipientId> recipientIds, Optional<String> name
282 ) {
283 return recipients.values()
284 .stream()
285 .filter(r -> !onlyContacts || r.getContact() != null)
286 .filter(r -> blocked.isEmpty() || (
287 blocked.get() == (
288 r.getContact() != null && r.getContact().isBlocked()
289 )
290 ))
291 .filter(r -> recipientIds.isEmpty() || (recipientIds.contains(r.getRecipientId())))
292 .filter(r -> name.isEmpty()
293 || (r.getContact() != null && name.get().equals(r.getContact().getName()))
294 || (r.getProfile() != null && name.get().equals(r.getProfile().getDisplayName())))
295 .toList();
296 }
297
298 @Override
299 public void deleteContact(RecipientId recipientId) {
300 synchronized (recipients) {
301 final var recipient = recipients.get(recipientId);
302 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(null).build());
303 }
304 }
305
306 public void deleteRecipientData(RecipientId recipientId) {
307 synchronized (recipients) {
308 logger.debug("Deleting recipient data for {}", recipientId);
309 final var recipient = recipients.get(recipientId);
310 recipient.getAddress()
311 .uuid()
312 .ifPresent(uuid -> storeRecipientLocked(recipientId,
313 Recipient.newBuilder()
314 .withRecipientId(recipientId)
315 .withAddress(new RecipientAddress(uuid))
316 .build()));
317 }
318 }
319
320 @Override
321 public Profile getProfile(final RecipientId recipientId) {
322 final var recipient = getRecipient(recipientId);
323 return recipient == null ? null : recipient.getProfile();
324 }
325
326 @Override
327 public ProfileKey getProfileKey(final RecipientId recipientId) {
328 final var recipient = getRecipient(recipientId);
329 return recipient == null ? null : recipient.getProfileKey();
330 }
331
332 @Override
333 public ProfileKeyCredential getProfileKeyCredential(final RecipientId recipientId) {
334 final var recipient = getRecipient(recipientId);
335 return recipient == null ? null : recipient.getProfileKeyCredential();
336 }
337
338 @Override
339 public void storeProfile(RecipientId recipientId, final Profile profile) {
340 synchronized (recipients) {
341 final var recipient = recipients.get(recipientId);
342 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfile(profile).build());
343 }
344 }
345
346 @Override
347 public void storeSelfProfileKey(final RecipientId recipientId, final ProfileKey profileKey) {
348 storeProfileKey(recipientId, profileKey, false);
349 }
350
351 @Override
352 public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
353 storeProfileKey(recipientId, profileKey, true);
354 }
355
356 private void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile) {
357 synchronized (recipients) {
358 final var recipient = recipients.get(recipientId);
359 if (profileKey != null && profileKey.equals(recipient.getProfileKey()) && (
360 recipient.getProfile() == null || (
361 recipient.getProfile().getUnidentifiedAccessMode() != Profile.UnidentifiedAccessMode.UNKNOWN
362 && recipient.getProfile().getUnidentifiedAccessMode()
363 != Profile.UnidentifiedAccessMode.DISABLED
364 )
365 )) {
366 return;
367 }
368
369 final var builder = Recipient.newBuilder(recipient)
370 .withProfileKey(profileKey)
371 .withProfileKeyCredential(null);
372 if (resetProfile) {
373 builder.withProfile(recipient.getProfile() == null
374 ? null
375 : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build());
376 }
377 final var newRecipient = builder.build();
378 storeRecipientLocked(recipientId, newRecipient);
379 }
380 }
381
382 @Override
383 public void storeProfileKeyCredential(RecipientId recipientId, final ProfileKeyCredential profileKeyCredential) {
384 synchronized (recipients) {
385 final var recipient = recipients.get(recipientId);
386 storeRecipientLocked(recipientId,
387 Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build());
388 }
389 }
390
391 public boolean isEmpty() {
392 synchronized (recipients) {
393 return recipients.isEmpty();
394 }
395 }
396
397 private void addRecipients(final Map<RecipientId, Recipient> recipients) {
398 this.recipients.putAll(recipients);
399 }
400
401 /**
402 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
403 * Has no effect, if the address contains only a number or a uuid.
404 */
405 private RecipientId resolveRecipient(RecipientAddress address, boolean isHighTrust, boolean isSelf) {
406 final Pair<RecipientId, Optional<RecipientId>> pair;
407 synchronized (recipients) {
408 pair = resolveRecipientLocked(address, isHighTrust, isSelf);
409 }
410
411 if (pair.second().isPresent()) {
412 recipientMergeHandler.mergeRecipients(pair.first(), pair.second().get());
413 }
414 return pair.first();
415 }
416
417 private Pair<RecipientId, Optional<RecipientId>> resolveRecipientLocked(
418 RecipientAddress address, boolean isHighTrust, boolean isSelf
419 ) {
420 if (isHighTrust && !isSelf) {
421 if (selfAddressProvider.getSelfAddress().matches(address)) {
422 isHighTrust = false;
423 }
424 }
425 final var byNumber = address.number().isEmpty()
426 ? Optional.<Recipient>empty()
427 : findByNumberLocked(address.number().get());
428 final var byUuid = address.uuid().isEmpty()
429 ? Optional.<Recipient>empty()
430 : findByUuidLocked(address.uuid().get());
431
432 if (byNumber.isEmpty() && byUuid.isEmpty()) {
433 logger.debug("Got new recipient, both uuid and number are unknown");
434
435 if (isHighTrust || address.uuid().isEmpty() || address.number().isEmpty()) {
436 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
437 }
438
439 return new Pair<>(addNewRecipientLocked(new RecipientAddress(address.uuid().get())), Optional.empty());
440 }
441
442 if (!isHighTrust || address.uuid().isEmpty() || address.number().isEmpty() || byNumber.equals(byUuid)) {
443 return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
444 }
445
446 if (byNumber.isEmpty()) {
447 logger.debug("Got recipient {} existing with uuid, updating with high trust number",
448 byUuid.get().getRecipientId());
449 updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
450 return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
451 }
452
453 final var byNumberRecipient = byNumber.get();
454
455 if (byUuid.isEmpty()) {
456 if (byNumberRecipient.getAddress().uuid().isPresent()) {
457 logger.debug(
458 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
459 byNumberRecipient.getRecipientId());
460
461 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
462 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
463 return new Pair<>(addNewRecipientLocked(address), Optional.empty());
464 }
465
466 logger.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
467 byNumberRecipient.getRecipientId());
468 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(), address);
469 return new Pair<>(byNumberRecipient.getRecipientId(), Optional.empty());
470 }
471
472 final var byUuidRecipient = byUuid.get();
473
474 if (byNumberRecipient.getAddress().uuid().isPresent()) {
475 logger.debug(
476 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
477 byNumberRecipient.getRecipientId(),
478 byUuidRecipient.getRecipientId());
479
480 updateRecipientAddressLocked(byNumberRecipient.getRecipientId(),
481 new RecipientAddress(byNumberRecipient.getAddress().uuid().get()));
482 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
483 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.empty());
484 }
485
486 logger.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
487 byNumberRecipient.getRecipientId(),
488 byUuidRecipient.getRecipientId());
489 updateRecipientAddressLocked(byUuidRecipient.getRecipientId(), address);
490 // Create a fixed RecipientId that won't update its id after merge
491 final var toBeMergedRecipientId = new RecipientId(byNumberRecipient.getRecipientId().id(), null);
492 mergeRecipientsLocked(byUuidRecipient.getRecipientId(), toBeMergedRecipientId);
493 return new Pair<>(byUuidRecipient.getRecipientId(), Optional.of(toBeMergedRecipientId));
494 }
495
496 private RecipientId addNewRecipientLocked(final RecipientAddress address) {
497 final var nextRecipientId = nextIdLocked();
498 logger.debug("Adding new recipient {} with address {}", nextRecipientId, address);
499 storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, address, null, null, null, null));
500 return nextRecipientId;
501 }
502
503 private void updateRecipientAddressLocked(RecipientId recipientId, final RecipientAddress address) {
504 final var recipient = recipients.get(recipientId);
505 storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build());
506 }
507
508 long getActualRecipientId(long recipientId) {
509 while (recipientsMerged.containsKey(recipientId)) {
510 final var newRecipientId = recipientsMerged.get(recipientId);
511 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
512 recipientId = newRecipientId;
513 }
514 return recipientId;
515 }
516
517 private void storeRecipientLocked(final RecipientId recipientId, final Recipient recipient) {
518 final var existingRecipient = recipients.get(recipientId);
519 if (existingRecipient == null || !existingRecipient.equals(recipient)) {
520 recipients.put(recipientId, recipient);
521 saveLocked();
522 }
523 }
524
525 private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
526 final var recipient = recipients.get(recipientId);
527 final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
528 recipients.put(recipientId,
529 new Recipient(recipientId,
530 recipient.getAddress(),
531 recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
532 recipient.getProfileKey() != null
533 ? recipient.getProfileKey()
534 : toBeMergedRecipient.getProfileKey(),
535 recipient.getProfileKeyCredential() != null
536 ? recipient.getProfileKeyCredential()
537 : toBeMergedRecipient.getProfileKeyCredential(),
538 recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
539 recipients.remove(toBeMergedRecipientId);
540 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
541 saveLocked();
542 }
543
544 private Optional<Recipient> findByNumberLocked(final String number) {
545 return recipients.entrySet()
546 .stream()
547 .filter(entry -> entry.getValue().getAddress().number().isPresent() && number.equals(entry.getValue()
548 .getAddress()
549 .number()
550 .get()))
551 .findFirst()
552 .map(Map.Entry::getValue);
553 }
554
555 private Optional<Recipient> findByUuidLocked(final UUID uuid) {
556 return recipients.entrySet()
557 .stream()
558 .filter(entry -> entry.getValue().getAddress().uuid().isPresent() && uuid.equals(entry.getValue()
559 .getAddress()
560 .uuid()
561 .get()))
562 .findFirst()
563 .map(Map.Entry::getValue);
564 }
565
566 private RecipientId nextIdLocked() {
567 return new RecipientId(++this.lastId, this);
568 }
569
570 private void saveLocked() {
571 if (isBulkUpdating) {
572 return;
573 }
574 final var base64 = Base64.getEncoder();
575 var storage = new Storage(recipients.entrySet().stream().map(pair -> {
576 final var recipient = pair.getValue();
577 final var recipientContact = recipient.getContact();
578 final var contact = recipientContact == null
579 ? null
580 : new Storage.Recipient.Contact(recipientContact.getName(),
581 recipientContact.getColor(),
582 recipientContact.getMessageExpirationTime(),
583 recipientContact.isBlocked(),
584 recipientContact.isArchived(),
585 recipientContact.isProfileSharingEnabled());
586 final var recipientProfile = recipient.getProfile();
587 final var profile = recipientProfile == null
588 ? null
589 : new Storage.Recipient.Profile(recipientProfile.getLastUpdateTimestamp(),
590 recipientProfile.getGivenName(),
591 recipientProfile.getFamilyName(),
592 recipientProfile.getAbout(),
593 recipientProfile.getAboutEmoji(),
594 recipientProfile.getAvatarUrlPath(),
595 recipientProfile.getMobileCoinAddress() == null
596 ? null
597 : base64.encodeToString(recipientProfile.getMobileCoinAddress()),
598 recipientProfile.getUnidentifiedAccessMode().name(),
599 recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet()));
600 return new Storage.Recipient(pair.getKey().id(),
601 recipient.getAddress().number().orElse(null),
602 recipient.getAddress().uuid().map(UUID::toString).orElse(null),
603 recipient.getProfileKey() == null
604 ? null
605 : base64.encodeToString(recipient.getProfileKey().serialize()),
606 recipient.getProfileKeyCredential() == null
607 ? null
608 : base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
609 contact,
610 profile);
611 }).toList(), lastId);
612
613 // Write to memory first to prevent corrupting the file in case of serialization errors
614 try (var inMemoryOutput = new ByteArrayOutputStream()) {
615 objectMapper.writeValue(inMemoryOutput, storage);
616
617 var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
618 try (var outputStream = new FileOutputStream(file)) {
619 input.transferTo(outputStream);
620 }
621 } catch (Exception e) {
622 logger.error("Error saving recipient store file: {}", e.getMessage());
623 }
624 }
625
626 private record Storage(List<Recipient> recipients, long lastId) {
627
628 private record Recipient(
629 long id,
630 String number,
631 String uuid,
632 String profileKey,
633 String profileKeyCredential,
634 Storage.Recipient.Contact contact,
635 Storage.Recipient.Profile profile
636 ) {
637
638 private record Contact(
639 String name,
640 String color,
641 int messageExpirationTime,
642 boolean blocked,
643 boolean archived,
644 boolean profileSharingEnabled
645 ) {}
646
647 private record Profile(
648 long lastUpdateTimestamp,
649 String givenName,
650 String familyName,
651 String about,
652 String aboutEmoji,
653 String avatarUrlPath,
654 String mobileCoinAddress,
655 String unidentifiedAccessMode,
656 Set<String> capabilities
657 ) {}
658 }
659 }
660
661 public interface RecipientMergeHandler {
662
663 void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId);
664 }
665 }