]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
926f09bd26959964f2f39c6a8d80ca094e6d9ad8
[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 org.asamk.signal.manager.api.Contact;
4 import org.asamk.signal.manager.api.Pair;
5 import org.asamk.signal.manager.api.Profile;
6 import org.asamk.signal.manager.api.UnregisteredRecipientException;
7 import org.asamk.signal.manager.storage.Database;
8 import org.asamk.signal.manager.storage.Utils;
9 import org.asamk.signal.manager.storage.contacts.ContactsStore;
10 import org.asamk.signal.manager.storage.profiles.ProfileStore;
11 import org.asamk.signal.manager.util.KeyUtils;
12 import org.signal.libsignal.zkgroup.InvalidInputException;
13 import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
14 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
15 import org.slf4j.Logger;
16 import org.slf4j.LoggerFactory;
17 import org.whispersystems.signalservice.api.push.ServiceId;
18 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
19 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
20 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
21 import org.whispersystems.signalservice.api.storage.StorageId;
22 import org.whispersystems.signalservice.api.util.UuidUtil;
23
24 import java.sql.Connection;
25 import java.sql.ResultSet;
26 import java.sql.SQLException;
27 import java.sql.Types;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Objects;
35 import java.util.Optional;
36 import java.util.Set;
37 import java.util.function.Supplier;
38 import java.util.stream.Collectors;
39
40 public class RecipientStore implements RecipientIdCreator, RecipientResolver, RecipientTrustedResolver, ContactsStore, ProfileStore {
41
42 private static final Logger logger = LoggerFactory.getLogger(RecipientStore.class);
43 private static final String TABLE_RECIPIENT = "recipient";
44 private static final String SQL_IS_CONTACT = "r.given_name IS NOT NULL OR r.family_name IS NOT NULL OR r.expiration_time > 0 OR r.profile_sharing = TRUE OR r.color IS NOT NULL OR r.blocked = TRUE OR r.archived = TRUE";
45
46 private final RecipientMergeHandler recipientMergeHandler;
47 private final SelfAddressProvider selfAddressProvider;
48 private final SelfProfileKeyProvider selfProfileKeyProvider;
49 private final Database database;
50
51 private final Object recipientsLock = new Object();
52 private final Map<Long, Long> recipientsMerged = new HashMap<>();
53
54 private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = new HashMap<>();
55
56 public static void createSql(Connection connection) throws SQLException {
57 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
58 try (final var statement = connection.createStatement()) {
59 statement.executeUpdate("""
60 CREATE TABLE recipient (
61 _id INTEGER PRIMARY KEY AUTOINCREMENT,
62 storage_id BLOB UNIQUE,
63 storage_record BLOB,
64 number TEXT UNIQUE,
65 username TEXT UNIQUE,
66 uuid BLOB UNIQUE,
67 pni BLOB UNIQUE,
68 profile_key BLOB,
69 profile_key_credential BLOB,
70
71 given_name TEXT,
72 family_name TEXT,
73 color TEXT,
74
75 expiration_time INTEGER NOT NULL DEFAULT 0,
76 blocked INTEGER NOT NULL DEFAULT FALSE,
77 archived INTEGER NOT NULL DEFAULT FALSE,
78 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
79 hidden INTEGER NOT NULL DEFAULT FALSE,
80
81 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
82 profile_given_name TEXT,
83 profile_family_name TEXT,
84 profile_about TEXT,
85 profile_about_emoji TEXT,
86 profile_avatar_url_path TEXT,
87 profile_mobile_coin_address BLOB,
88 profile_unidentified_access_mode TEXT,
89 profile_capabilities TEXT
90 ) STRICT;
91 """);
92 }
93 }
94
95 public RecipientStore(
96 final RecipientMergeHandler recipientMergeHandler,
97 final SelfAddressProvider selfAddressProvider,
98 final SelfProfileKeyProvider selfProfileKeyProvider,
99 final Database database
100 ) {
101 this.recipientMergeHandler = recipientMergeHandler;
102 this.selfAddressProvider = selfAddressProvider;
103 this.selfProfileKeyProvider = selfProfileKeyProvider;
104 this.database = database;
105 }
106
107 public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
108 final var sql = (
109 """
110 SELECT r.number, r.uuid, r.pni, r.username
111 FROM %s r
112 WHERE r._id = ?
113 """
114 ).formatted(TABLE_RECIPIENT);
115 try (final var connection = database.getConnection()) {
116 try (final var statement = connection.prepareStatement(sql)) {
117 statement.setLong(1, recipientId.id());
118 return Utils.executeQuerySingleRow(statement, this::getRecipientAddressFromResultSet);
119 }
120 } catch (SQLException e) {
121 throw new RuntimeException("Failed read from recipient store", e);
122 }
123 }
124
125 public Collection<RecipientId> getRecipientIdsWithEnabledProfileSharing() {
126 final var sql = (
127 """
128 SELECT r._id
129 FROM %s r
130 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
131 """
132 ).formatted(TABLE_RECIPIENT);
133 try (final var connection = database.getConnection()) {
134 try (final var statement = connection.prepareStatement(sql)) {
135 try (var result = Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet)) {
136 return result.toList();
137 }
138 }
139 } catch (SQLException e) {
140 throw new RuntimeException("Failed read from recipient store", e);
141 }
142 }
143
144 @Override
145 public RecipientId resolveRecipient(final long rawRecipientId) {
146 final var sql = (
147 """
148 SELECT r._id
149 FROM %s r
150 WHERE r._id = ?
151 """
152 ).formatted(TABLE_RECIPIENT);
153 try (final var connection = database.getConnection()) {
154 try (final var statement = connection.prepareStatement(sql)) {
155 statement.setLong(1, rawRecipientId);
156 return Utils.executeQueryForOptional(statement, this::getRecipientIdFromResultSet).orElse(null);
157 }
158 } catch (SQLException e) {
159 throw new RuntimeException("Failed read from recipient store", e);
160 }
161 }
162
163 @Override
164 public RecipientId resolveRecipient(final String identifier) {
165 final var serviceId = ServiceId.parseOrNull(identifier);
166 if (serviceId != null) {
167 return resolveRecipient(serviceId);
168 } else {
169 return resolveRecipientByNumber(identifier);
170 }
171 }
172
173 private RecipientId resolveRecipientByNumber(final String number) {
174 synchronized (recipientsLock) {
175 final RecipientId recipientId;
176 try (final var connection = database.getConnection()) {
177 connection.setAutoCommit(false);
178 recipientId = resolveRecipientLocked(connection, number);
179 connection.commit();
180 } catch (SQLException e) {
181 throw new RuntimeException("Failed read recipient store", e);
182 }
183 return recipientId;
184 }
185 }
186
187 @Override
188 public RecipientId resolveRecipient(final ServiceId serviceId) {
189 synchronized (recipientsLock) {
190 final var recipientWithAddress = recipientAddressCache.get(serviceId);
191 if (recipientWithAddress != null) {
192 return recipientWithAddress.id();
193 }
194 try (final var connection = database.getConnection()) {
195 connection.setAutoCommit(false);
196 final var recipientId = resolveRecipientLocked(connection, serviceId);
197 connection.commit();
198 return recipientId;
199 } catch (SQLException e) {
200 throw new RuntimeException("Failed read recipient store", e);
201 }
202 }
203 }
204
205 /**
206 * Should only be used for recipientIds from the database.
207 * Where the foreign key relations ensure a valid recipientId.
208 */
209 @Override
210 public RecipientId create(final long recipientId) {
211 return new RecipientId(recipientId, this);
212 }
213
214 public RecipientId resolveRecipientByNumber(
215 final String number, Supplier<ServiceId> serviceIdSupplier
216 ) throws UnregisteredRecipientException {
217 final Optional<RecipientWithAddress> byNumber;
218 try (final var connection = database.getConnection()) {
219 byNumber = findByNumber(connection, number);
220 } catch (SQLException e) {
221 throw new RuntimeException("Failed read from recipient store", e);
222 }
223 if (byNumber.isEmpty() || byNumber.get().address().serviceId().isEmpty()) {
224 final var serviceId = serviceIdSupplier.get();
225 if (serviceId == null) {
226 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
227 number));
228 }
229
230 return resolveRecipient(serviceId);
231 }
232 return byNumber.get().id();
233 }
234
235 public Optional<RecipientId> resolveRecipientByNumberOptional(final String number) {
236 final Optional<RecipientWithAddress> byNumber;
237 try (final var connection = database.getConnection()) {
238 byNumber = findByNumber(connection, number);
239 } catch (SQLException e) {
240 throw new RuntimeException("Failed read from recipient store", e);
241 }
242 return byNumber.map(RecipientWithAddress::id);
243 }
244
245 public RecipientId resolveRecipientByUsername(
246 final String username, Supplier<ACI> aciSupplier
247 ) throws UnregisteredRecipientException {
248 final Optional<RecipientWithAddress> byUsername;
249 try (final var connection = database.getConnection()) {
250 byUsername = findByUsername(connection, username);
251 } catch (SQLException e) {
252 throw new RuntimeException("Failed read from recipient store", e);
253 }
254 if (byUsername.isEmpty() || byUsername.get().address().serviceId().isEmpty()) {
255 final var aci = aciSupplier.get();
256 if (aci == null) {
257 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
258 null,
259 username));
260 }
261
262 return resolveRecipientTrusted(aci, username);
263 }
264 return byUsername.get().id();
265 }
266
267 public RecipientId resolveRecipient(RecipientAddress address) {
268 synchronized (recipientsLock) {
269 final RecipientId recipientId;
270 try (final var connection = database.getConnection()) {
271 connection.setAutoCommit(false);
272 recipientId = resolveRecipientLocked(connection, address);
273 connection.commit();
274 } catch (SQLException e) {
275 throw new RuntimeException("Failed read recipient store", e);
276 }
277 return recipientId;
278 }
279 }
280
281 public RecipientId resolveRecipient(Connection connection, RecipientAddress address) throws SQLException {
282 return resolveRecipientLocked(connection, address);
283 }
284
285 @Override
286 public RecipientId resolveSelfRecipientTrusted(RecipientAddress address) {
287 return resolveRecipientTrusted(address, true);
288 }
289
290 @Override
291 public RecipientId resolveRecipientTrusted(RecipientAddress address) {
292 return resolveRecipientTrusted(address, false);
293 }
294
295 public RecipientId resolveRecipientTrusted(Connection connection, RecipientAddress address) throws SQLException {
296 final var pair = resolveRecipientTrustedLocked(connection, address, false);
297 if (!pair.second().isEmpty()) {
298 mergeRecipients(connection, pair.first(), pair.second());
299 }
300 return pair.first();
301 }
302
303 @Override
304 public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
305 return resolveRecipientTrusted(new RecipientAddress(address));
306 }
307
308 @Override
309 public RecipientId resolveRecipientTrusted(
310 final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
311 ) {
312 final var serviceId = aci.map(a -> (ServiceId) a).or(() -> pni);
313 return resolveRecipientTrusted(new RecipientAddress(serviceId, pni, number, Optional.empty()));
314 }
315
316 @Override
317 public RecipientId resolveRecipientTrusted(final ACI aci, final String username) {
318 return resolveRecipientTrusted(new RecipientAddress(aci, null, null, username));
319 }
320
321 @Override
322 public void storeContact(RecipientId recipientId, final Contact contact) {
323 try (final var connection = database.getConnection()) {
324 storeContact(connection, recipientId, contact);
325 } catch (SQLException e) {
326 throw new RuntimeException("Failed update recipient store", e);
327 }
328 }
329
330 @Override
331 public Contact getContact(RecipientId recipientId) {
332 try (final var connection = database.getConnection()) {
333 return getContact(connection, recipientId);
334 } catch (SQLException e) {
335 throw new RuntimeException("Failed read from recipient store", e);
336 }
337 }
338
339 @Override
340 public List<Pair<RecipientId, Contact>> getContacts() {
341 final var sql = (
342 """
343 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
344 FROM %s r
345 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s AND r.hidden = FALSE
346 """
347 ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
348 try (final var connection = database.getConnection()) {
349 try (final var statement = connection.prepareStatement(sql)) {
350 try (var result = Utils.executeQueryForStream(statement,
351 resultSet -> new Pair<>(getRecipientIdFromResultSet(resultSet),
352 getContactFromResultSet(resultSet)))) {
353 return result.toList();
354 }
355 }
356 } catch (SQLException e) {
357 throw new RuntimeException("Failed read from recipient store", e);
358 }
359 }
360
361 public Recipient getRecipient(Connection connection, RecipientId recipientId) throws SQLException {
362 final var sql = (
363 """
364 SELECT r._id,
365 r.number, r.uuid, r.pni, r.username,
366 r.profile_key, r.profile_key_credential,
367 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
368 r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
369 r.storage_record
370 FROM %s r
371 WHERE r._id = ?
372 """
373 ).formatted(TABLE_RECIPIENT);
374 try (final var statement = connection.prepareStatement(sql)) {
375 statement.setLong(1, recipientId.id());
376 return Utils.executeQuerySingleRow(statement, this::getRecipientFromResultSet);
377 }
378 }
379
380 public Recipient getRecipient(Connection connection, StorageId storageId) throws SQLException {
381 final var sql = (
382 """
383 SELECT r._id,
384 r.number, r.uuid, r.pni, r.username,
385 r.profile_key, r.profile_key_credential,
386 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
387 r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
388 r.storage_record
389 FROM %s r
390 WHERE r.storage_id = ?
391 """
392 ).formatted(TABLE_RECIPIENT);
393 try (final var statement = connection.prepareStatement(sql)) {
394 statement.setBytes(1, storageId.getRaw());
395 return Utils.executeQuerySingleRow(statement, this::getRecipientFromResultSet);
396 }
397 }
398
399 public List<Recipient> getRecipients(
400 boolean onlyContacts, Optional<Boolean> blocked, Set<RecipientId> recipientIds, Optional<String> name
401 ) {
402 final var sqlWhere = new ArrayList<String>();
403 if (onlyContacts) {
404 sqlWhere.add("(" + SQL_IS_CONTACT + ")");
405 sqlWhere.add("r.hidden = FALSE");
406 }
407 if (blocked.isPresent()) {
408 sqlWhere.add("r.blocked = ?");
409 }
410 if (!recipientIds.isEmpty()) {
411 final var recipientIdsCommaSeparated = recipientIds.stream()
412 .map(recipientId -> String.valueOf(recipientId.id()))
413 .collect(Collectors.joining(","));
414 sqlWhere.add("r._id IN (" + recipientIdsCommaSeparated + ")");
415 }
416 final var sql = (
417 """
418 SELECT r._id,
419 r.number, r.uuid, r.pni, r.username,
420 r.profile_key, r.profile_key_credential,
421 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
422 r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
423 r.storage_record
424 FROM %s r
425 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
426 """
427 ).formatted(TABLE_RECIPIENT, sqlWhere.isEmpty() ? "TRUE" : String.join(" AND ", sqlWhere));
428 final var selfServiceId = selfAddressProvider.getSelfAddress().serviceId();
429 try (final var connection = database.getConnection()) {
430 try (final var statement = connection.prepareStatement(sql)) {
431 if (blocked.isPresent()) {
432 statement.setBoolean(1, blocked.get());
433 }
434 try (var result = Utils.executeQueryForStream(statement, this::getRecipientFromResultSet)) {
435 return result.filter(r -> name.isEmpty() || (
436 r.getContact() != null && name.get().equals(r.getContact().getName())
437 ) || (r.getProfile() != null && name.get().equals(r.getProfile().getDisplayName()))).map(r -> {
438 if (r.getAddress().serviceId().equals(selfServiceId)) {
439 return Recipient.newBuilder(r)
440 .withProfileKey(selfProfileKeyProvider.getSelfProfileKey())
441 .build();
442 }
443 return r;
444 }).toList();
445 }
446 }
447 } catch (SQLException e) {
448 throw new RuntimeException("Failed read from recipient store", e);
449 }
450 }
451
452 public Set<String> getAllNumbers() {
453 final var sql = (
454 """
455 SELECT r.number
456 FROM %s r
457 WHERE r.number IS NOT NULL
458 """
459 ).formatted(TABLE_RECIPIENT);
460 final var selfNumber = selfAddressProvider.getSelfAddress().number().orElse(null);
461 try (final var connection = database.getConnection()) {
462 try (final var statement = connection.prepareStatement(sql)) {
463 return Utils.executeQueryForStream(statement, resultSet -> resultSet.getString("number"))
464 .filter(Objects::nonNull)
465 .filter(n -> !n.equals(selfNumber))
466 .filter(n -> {
467 try {
468 Long.parseLong(n);
469 return true;
470 } catch (NumberFormatException e) {
471 return false;
472 }
473 })
474 .collect(Collectors.toSet());
475 }
476 } catch (SQLException e) {
477 throw new RuntimeException("Failed read from recipient store", e);
478 }
479 }
480
481 public Map<ServiceId, ProfileKey> getServiceIdToProfileKeyMap() {
482 final var sql = (
483 """
484 SELECT r.uuid, r.profile_key
485 FROM %s r
486 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
487 """
488 ).formatted(TABLE_RECIPIENT);
489 final var selfServiceId = selfAddressProvider.getSelfAddress().serviceId().orElse(null);
490 try (final var connection = database.getConnection()) {
491 try (final var statement = connection.prepareStatement(sql)) {
492 return Utils.executeQueryForStream(statement, resultSet -> {
493 final var serviceId = ServiceId.parseOrThrow(resultSet.getBytes("uuid"));
494 if (serviceId.equals(selfServiceId)) {
495 return new Pair<>(serviceId, selfProfileKeyProvider.getSelfProfileKey());
496 }
497 final var profileKey = getProfileKeyFromResultSet(resultSet);
498 return new Pair<>(serviceId, profileKey);
499 }).filter(Objects::nonNull).collect(Collectors.toMap(Pair::first, Pair::second));
500 }
501 } catch (SQLException e) {
502 throw new RuntimeException("Failed read from recipient store", e);
503 }
504 }
505
506 public List<RecipientId> getRecipientIds(Connection connection) throws SQLException {
507 final var sql = (
508 """
509 SELECT r._id
510 FROM %s r
511 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL)
512 """
513 ).formatted(TABLE_RECIPIENT);
514 try (final var statement = connection.prepareStatement(sql)) {
515 return Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet).toList();
516 }
517 }
518
519 public void setMissingStorageIds() {
520 final var selectSql = (
521 """
522 SELECT r._id
523 FROM %s r
524 WHERE r.storage_id IS NULL
525 """
526 ).formatted(TABLE_RECIPIENT);
527 final var updateSql = (
528 """
529 UPDATE %s
530 SET storage_id = ?
531 WHERE _id = ?
532 """
533 ).formatted(TABLE_RECIPIENT);
534 try (final var connection = database.getConnection()) {
535 connection.setAutoCommit(false);
536 try (final var selectStmt = connection.prepareStatement(selectSql)) {
537 final var recipientIds = Utils.executeQueryForStream(selectStmt, this::getRecipientIdFromResultSet)
538 .toList();
539 try (final var updateStmt = connection.prepareStatement(updateSql)) {
540 for (final var recipientId : recipientIds) {
541 updateStmt.setBytes(1, KeyUtils.createRawStorageId());
542 updateStmt.setLong(2, recipientId.id());
543 updateStmt.executeUpdate();
544 }
545 }
546 }
547 connection.commit();
548 } catch (SQLException e) {
549 throw new RuntimeException("Failed update recipient store", e);
550 }
551 }
552
553 @Override
554 public void deleteContact(RecipientId recipientId) {
555 storeContact(recipientId, null);
556 }
557
558 public void deleteRecipientData(RecipientId recipientId) {
559 logger.debug("Deleting recipient data for {}", recipientId);
560 synchronized (recipientsLock) {
561 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
562 try (final var connection = database.getConnection()) {
563 connection.setAutoCommit(false);
564 storeContact(connection, recipientId, null);
565 storeProfile(connection, recipientId, null);
566 storeProfileKey(connection, recipientId, null, false);
567 storeExpiringProfileKeyCredential(connection, recipientId, null);
568 deleteRecipient(connection, recipientId);
569 connection.commit();
570 } catch (SQLException e) {
571 throw new RuntimeException("Failed update recipient store", e);
572 }
573 }
574 }
575
576 @Override
577 public Profile getProfile(final RecipientId recipientId) {
578 try (final var connection = database.getConnection()) {
579 return getProfile(connection, recipientId);
580 } catch (SQLException e) {
581 throw new RuntimeException("Failed read from recipient store", e);
582 }
583 }
584
585 @Override
586 public ProfileKey getProfileKey(final RecipientId recipientId) {
587 try (final var connection = database.getConnection()) {
588 return getProfileKey(connection, recipientId);
589 } catch (SQLException e) {
590 throw new RuntimeException("Failed read from recipient store", e);
591 }
592 }
593
594 @Override
595 public ExpiringProfileKeyCredential getExpiringProfileKeyCredential(final RecipientId recipientId) {
596 try (final var connection = database.getConnection()) {
597 return getExpiringProfileKeyCredential(connection, recipientId);
598 } catch (SQLException e) {
599 throw new RuntimeException("Failed read from recipient store", e);
600 }
601 }
602
603 @Override
604 public void storeProfile(RecipientId recipientId, final Profile profile) {
605 try (final var connection = database.getConnection()) {
606 storeProfile(connection, recipientId, profile);
607 } catch (SQLException e) {
608 throw new RuntimeException("Failed update recipient store", e);
609 }
610 }
611
612 @Override
613 public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
614 try (final var connection = database.getConnection()) {
615 storeProfileKey(connection, recipientId, profileKey);
616 } catch (SQLException e) {
617 throw new RuntimeException("Failed update recipient store", e);
618 }
619 }
620
621 public void storeProfileKey(
622 Connection connection, RecipientId recipientId, final ProfileKey profileKey
623 ) throws SQLException {
624 storeProfileKey(connection, recipientId, profileKey, true);
625 }
626
627 @Override
628 public void storeExpiringProfileKeyCredential(
629 RecipientId recipientId, final ExpiringProfileKeyCredential profileKeyCredential
630 ) {
631 try (final var connection = database.getConnection()) {
632 storeExpiringProfileKeyCredential(connection, recipientId, profileKeyCredential);
633 } catch (SQLException e) {
634 throw new RuntimeException("Failed update recipient store", e);
635 }
636 }
637
638 public void rotateSelfStorageId() {
639 try (final var connection = database.getConnection()) {
640 rotateSelfStorageId(connection);
641 } catch (SQLException e) {
642 throw new RuntimeException("Failed update recipient store", e);
643 }
644 }
645
646 public void rotateSelfStorageId(final Connection connection) throws SQLException {
647 final var selfRecipientId = resolveRecipient(connection, selfAddressProvider.getSelfAddress());
648 rotateStorageId(connection, selfRecipientId);
649 }
650
651 public StorageId rotateStorageId(final Connection connection, final ServiceId serviceId) throws SQLException {
652 final var selfRecipientId = resolveRecipient(connection, new RecipientAddress(serviceId));
653 return rotateStorageId(connection, selfRecipientId);
654 }
655
656 public List<StorageId> getStorageIds(Connection connection) throws SQLException {
657 final var sql = """
658 SELECT r.storage_id
659 FROM %s r WHERE r.storage_id IS NOT NULL AND r._id != ? AND (r.uuid IS NOT NULL OR r.pni IS NOT NULL)
660 """.formatted(TABLE_RECIPIENT);
661 final var selfRecipientId = resolveRecipient(connection, selfAddressProvider.getSelfAddress());
662 try (final var statement = connection.prepareStatement(sql)) {
663 statement.setLong(1, selfRecipientId.id());
664 return Utils.executeQueryForStream(statement, this::getContactStorageIdFromResultSet).toList();
665 }
666 }
667
668 public void updateStorageId(
669 Connection connection, RecipientId recipientId, StorageId storageId
670 ) throws SQLException {
671 final var sql = (
672 """
673 UPDATE %s
674 SET storage_id = ?
675 WHERE _id = ?
676 """
677 ).formatted(TABLE_RECIPIENT);
678 try (final var statement = connection.prepareStatement(sql)) {
679 statement.setBytes(1, storageId.getRaw());
680 statement.setLong(2, recipientId.id());
681 statement.executeUpdate();
682 }
683 }
684
685 public void updateStorageIds(Connection connection, Map<RecipientId, StorageId> storageIdMap) throws SQLException {
686 final var sql = (
687 """
688 UPDATE %s
689 SET storage_id = ?
690 WHERE _id = ?
691 """
692 ).formatted(TABLE_RECIPIENT);
693 try (final var statement = connection.prepareStatement(sql)) {
694 for (final var entry : storageIdMap.entrySet()) {
695 statement.setBytes(1, entry.getValue().getRaw());
696 statement.setLong(2, entry.getKey().id());
697 statement.executeUpdate();
698 }
699 }
700 }
701
702 public StorageId getSelfStorageId(final Connection connection) throws SQLException {
703 final var selfRecipientId = resolveRecipient(connection, selfAddressProvider.getSelfAddress());
704 return StorageId.forAccount(getStorageId(connection, selfRecipientId).getRaw());
705 }
706
707 public StorageId getStorageId(final Connection connection, final RecipientId recipientId) throws SQLException {
708 final var sql = """
709 SELECT r.storage_id
710 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
711 """.formatted(TABLE_RECIPIENT);
712 try (final var statement = connection.prepareStatement(sql)) {
713 statement.setLong(1, recipientId.id());
714 final var storageId = Utils.executeQueryForOptional(statement, this::getContactStorageIdFromResultSet);
715 if (storageId.isPresent()) {
716 return storageId.get();
717 }
718 }
719 return rotateStorageId(connection, recipientId);
720 }
721
722 private StorageId rotateStorageId(final Connection connection, final RecipientId recipientId) throws SQLException {
723 final var newStorageId = StorageId.forAccount(KeyUtils.createRawStorageId());
724 updateStorageId(connection, recipientId, newStorageId);
725 return newStorageId;
726 }
727
728 public void storeStorageRecord(
729 final Connection connection,
730 final RecipientId recipientId,
731 final StorageId storageId,
732 final byte[] storageRecord
733 ) throws SQLException {
734 final var sql = (
735 """
736 UPDATE %s
737 SET storage_id = ?, storage_record = ?
738 WHERE _id = ?
739 """
740 ).formatted(TABLE_RECIPIENT);
741 try (final var statement = connection.prepareStatement(sql)) {
742 statement.setBytes(1, storageId.getRaw());
743 if (storageRecord == null) {
744 statement.setNull(2, Types.BLOB);
745 } else {
746 statement.setBytes(2, storageRecord);
747 }
748 statement.setLong(3, recipientId.id());
749 statement.executeUpdate();
750 }
751 }
752
753 void addLegacyRecipients(final Map<RecipientId, Recipient> recipients) {
754 logger.debug("Migrating legacy recipients to database");
755 long start = System.nanoTime();
756 final var sql = (
757 """
758 INSERT INTO %s (_id, number, uuid)
759 VALUES (?, ?, ?)
760 """
761 ).formatted(TABLE_RECIPIENT);
762 try (final var connection = database.getConnection()) {
763 connection.setAutoCommit(false);
764 try (final var statement = connection.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT))) {
765 statement.executeUpdate();
766 }
767 try (final var statement = connection.prepareStatement(sql)) {
768 for (final var recipient : recipients.values()) {
769 statement.setLong(1, recipient.getRecipientId().id());
770 statement.setString(2, recipient.getAddress().number().orElse(null));
771 statement.setBytes(3,
772 recipient.getAddress()
773 .serviceId()
774 .map(ServiceId::getRawUuid)
775 .map(UuidUtil::toByteArray)
776 .orElse(null));
777 statement.executeUpdate();
778 }
779 }
780 logger.debug("Initial inserts took {}ms", (System.nanoTime() - start) / 1000000);
781
782 for (final var recipient : recipients.values()) {
783 if (recipient.getContact() != null) {
784 storeContact(connection, recipient.getRecipientId(), recipient.getContact());
785 }
786 if (recipient.getProfile() != null) {
787 storeProfile(connection, recipient.getRecipientId(), recipient.getProfile());
788 }
789 if (recipient.getProfileKey() != null) {
790 storeProfileKey(connection, recipient.getRecipientId(), recipient.getProfileKey(), false);
791 }
792 if (recipient.getExpiringProfileKeyCredential() != null) {
793 storeExpiringProfileKeyCredential(connection,
794 recipient.getRecipientId(),
795 recipient.getExpiringProfileKeyCredential());
796 }
797 }
798 connection.commit();
799 } catch (SQLException e) {
800 throw new RuntimeException("Failed update recipient store", e);
801 }
802 logger.debug("Complete recipients migration took {}ms", (System.nanoTime() - start) / 1000000);
803 }
804
805 long getActualRecipientId(long recipientId) {
806 while (recipientsMerged.containsKey(recipientId)) {
807 final var newRecipientId = recipientsMerged.get(recipientId);
808 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
809 recipientId = newRecipientId;
810 }
811 return recipientId;
812 }
813
814 public void storeContact(
815 final Connection connection, final RecipientId recipientId, final Contact contact
816 ) throws SQLException {
817 final var sql = (
818 """
819 UPDATE %s
820 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
821 WHERE _id = ?
822 """
823 ).formatted(TABLE_RECIPIENT);
824 try (final var statement = connection.prepareStatement(sql)) {
825 statement.setString(1, contact == null ? null : contact.givenName());
826 statement.setString(2, contact == null ? null : contact.familyName());
827 statement.setInt(3, contact == null ? 0 : contact.messageExpirationTime());
828 statement.setBoolean(4, contact != null && contact.isProfileSharingEnabled());
829 statement.setString(5, contact == null ? null : contact.color());
830 statement.setBoolean(6, contact != null && contact.isBlocked());
831 statement.setBoolean(7, contact != null && contact.isArchived());
832 statement.setLong(8, recipientId.id());
833 statement.executeUpdate();
834 }
835 rotateStorageId(connection, recipientId);
836 }
837
838 private void storeExpiringProfileKeyCredential(
839 final Connection connection,
840 final RecipientId recipientId,
841 final ExpiringProfileKeyCredential profileKeyCredential
842 ) throws SQLException {
843 final var sql = (
844 """
845 UPDATE %s
846 SET profile_key_credential = ?
847 WHERE _id = ?
848 """
849 ).formatted(TABLE_RECIPIENT);
850 try (final var statement = connection.prepareStatement(sql)) {
851 statement.setBytes(1, profileKeyCredential == null ? null : profileKeyCredential.serialize());
852 statement.setLong(2, recipientId.id());
853 statement.executeUpdate();
854 }
855 }
856
857 public void storeProfile(
858 final Connection connection, final RecipientId recipientId, final Profile profile
859 ) throws SQLException {
860 final var sql = (
861 """
862 UPDATE %s
863 SET profile_last_update_timestamp = ?, profile_given_name = ?, profile_family_name = ?, profile_about = ?, profile_about_emoji = ?, profile_avatar_url_path = ?, profile_mobile_coin_address = ?, profile_unidentified_access_mode = ?, profile_capabilities = ?
864 WHERE _id = ?
865 """
866 ).formatted(TABLE_RECIPIENT);
867 try (final var statement = connection.prepareStatement(sql)) {
868 statement.setLong(1, profile == null ? 0 : profile.getLastUpdateTimestamp());
869 statement.setString(2, profile == null ? null : profile.getGivenName());
870 statement.setString(3, profile == null ? null : profile.getFamilyName());
871 statement.setString(4, profile == null ? null : profile.getAbout());
872 statement.setString(5, profile == null ? null : profile.getAboutEmoji());
873 statement.setString(6, profile == null ? null : profile.getAvatarUrlPath());
874 statement.setBytes(7, profile == null ? null : profile.getMobileCoinAddress());
875 statement.setString(8, profile == null ? null : profile.getUnidentifiedAccessMode().name());
876 statement.setString(9,
877 profile == null
878 ? null
879 : profile.getCapabilities().stream().map(Enum::name).collect(Collectors.joining(",")));
880 statement.setLong(10, recipientId.id());
881 statement.executeUpdate();
882 }
883 rotateStorageId(connection, recipientId);
884 }
885
886 private void storeProfileKey(
887 Connection connection, RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile
888 ) throws SQLException {
889 if (profileKey != null) {
890 final var recipientProfileKey = getProfileKey(connection, recipientId);
891 if (profileKey.equals(recipientProfileKey)) {
892 final var recipientProfile = getProfile(connection, recipientId);
893 if (recipientProfile == null || (
894 recipientProfile.getUnidentifiedAccessMode() != Profile.UnidentifiedAccessMode.UNKNOWN
895 && recipientProfile.getUnidentifiedAccessMode()
896 != Profile.UnidentifiedAccessMode.DISABLED
897 )) {
898 return;
899 }
900 }
901 }
902
903 final var sql = (
904 """
905 UPDATE %s
906 SET profile_key = ?, profile_key_credential = NULL%s
907 WHERE _id = ?
908 """
909 ).formatted(TABLE_RECIPIENT, resetProfile ? ", profile_last_update_timestamp = 0" : "");
910 try (final var statement = connection.prepareStatement(sql)) {
911 statement.setBytes(1, profileKey == null ? null : profileKey.serialize());
912 statement.setLong(2, recipientId.id());
913 statement.executeUpdate();
914 }
915 rotateStorageId(connection, recipientId);
916 }
917
918 private RecipientId resolveRecipientTrusted(RecipientAddress address, boolean isSelf) {
919 final Pair<RecipientId, List<RecipientId>> pair;
920 synchronized (recipientsLock) {
921 try (final var connection = database.getConnection()) {
922 connection.setAutoCommit(false);
923 pair = resolveRecipientTrustedLocked(connection, address, isSelf);
924 connection.commit();
925 } catch (SQLException e) {
926 throw new RuntimeException("Failed update recipient store", e);
927 }
928 }
929
930 if (!pair.second().isEmpty()) {
931 try (final var connection = database.getConnection()) {
932 connection.setAutoCommit(false);
933 mergeRecipients(connection, pair.first(), pair.second());
934 connection.commit();
935 } catch (SQLException e) {
936 throw new RuntimeException("Failed update recipient store", e);
937 }
938 }
939 return pair.first();
940 }
941
942 private Pair<RecipientId, List<RecipientId>> resolveRecipientTrustedLocked(
943 final Connection connection, final RecipientAddress address, final boolean isSelf
944 ) throws SQLException {
945 if (address.hasSingleIdentifier() || (
946 !isSelf && selfAddressProvider.getSelfAddress().matches(address)
947 )) {
948 return new Pair<>(resolveRecipientLocked(connection, address), List.of());
949 } else {
950 final var pair = MergeRecipientHelper.resolveRecipientTrustedLocked(new HelperStore(connection), address);
951
952 for (final var toBeMergedRecipientId : pair.second()) {
953 mergeRecipientsLocked(connection, pair.first(), toBeMergedRecipientId);
954 }
955 return pair;
956 }
957 }
958
959 private void mergeRecipients(
960 final Connection connection, final RecipientId recipientId, final List<RecipientId> toBeMergedRecipientIds
961 ) throws SQLException {
962 for (final var toBeMergedRecipientId : toBeMergedRecipientIds) {
963 recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId);
964 deleteRecipient(connection, toBeMergedRecipientId);
965 synchronized (recipientsLock) {
966 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId));
967 }
968 }
969 }
970
971 private RecipientId resolveRecipientLocked(
972 Connection connection, RecipientAddress address
973 ) throws SQLException {
974 final var aci = address.aci().isEmpty()
975 ? Optional.<RecipientWithAddress>empty()
976 : findByServiceId(connection, address.aci().get());
977
978 if (aci.isPresent()) {
979 return aci.get().id();
980 }
981
982 final var byPni = address.pni().isEmpty()
983 ? Optional.<RecipientWithAddress>empty()
984 : findByServiceId(connection, address.pni().get());
985
986 if (byPni.isPresent()) {
987 return byPni.get().id();
988 }
989
990 final var byNumber = address.number().isEmpty()
991 ? Optional.<RecipientWithAddress>empty()
992 : findByNumber(connection, address.number().get());
993
994 if (byNumber.isPresent()) {
995 return byNumber.get().id();
996 }
997
998 logger.debug("Got new recipient, both serviceId and number are unknown");
999
1000 if (address.serviceId().isEmpty()) {
1001 return addNewRecipient(connection, address);
1002 }
1003
1004 return addNewRecipient(connection, new RecipientAddress(address.serviceId().get()));
1005 }
1006
1007 private RecipientId resolveRecipientLocked(Connection connection, ServiceId serviceId) throws SQLException {
1008 final var recipient = findByServiceId(connection, serviceId);
1009
1010 if (recipient.isEmpty()) {
1011 logger.debug("Got new recipient, serviceId is unknown");
1012 return addNewRecipient(connection, new RecipientAddress(serviceId));
1013 }
1014
1015 return recipient.get().id();
1016 }
1017
1018 private RecipientId resolveRecipientLocked(Connection connection, String number) throws SQLException {
1019 final var recipient = findByNumber(connection, number);
1020
1021 if (recipient.isEmpty()) {
1022 logger.debug("Got new recipient, number is unknown");
1023 return addNewRecipient(connection, new RecipientAddress(null, number));
1024 }
1025
1026 return recipient.get().id();
1027 }
1028
1029 private RecipientId addNewRecipient(
1030 final Connection connection, final RecipientAddress address
1031 ) throws SQLException {
1032 final var sql = (
1033 """
1034 INSERT INTO %s (number, uuid, pni, username)
1035 VALUES (?, ?, ?, ?)
1036 RETURNING _id
1037 """
1038 ).formatted(TABLE_RECIPIENT);
1039 try (final var statement = connection.prepareStatement(sql)) {
1040 statement.setString(1, address.number().orElse(null));
1041 statement.setBytes(2, address.aci().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
1042 statement.setBytes(3, address.pni().map(PNI::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
1043 statement.setString(4, address.username().orElse(null));
1044 final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
1045 if (generatedKey.isPresent()) {
1046 final var recipientId = new RecipientId(generatedKey.get(), this);
1047 logger.debug("Added new recipient {} with address {}", recipientId, address);
1048 return recipientId;
1049 } else {
1050 throw new RuntimeException("Failed to add new recipient to database");
1051 }
1052 }
1053 }
1054
1055 private void removeRecipientAddress(Connection connection, RecipientId recipientId) throws SQLException {
1056 synchronized (recipientsLock) {
1057 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
1058 final var sql = (
1059 """
1060 UPDATE %s
1061 SET number = NULL, uuid = NULL, pni = NULL, username = NULL, storage_id = NULL
1062 WHERE _id = ?
1063 """
1064 ).formatted(TABLE_RECIPIENT);
1065 try (final var statement = connection.prepareStatement(sql)) {
1066 statement.setLong(1, recipientId.id());
1067 statement.executeUpdate();
1068 }
1069 }
1070 }
1071
1072 private void updateRecipientAddress(
1073 Connection connection, RecipientId recipientId, final RecipientAddress address
1074 ) throws SQLException {
1075 synchronized (recipientsLock) {
1076 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
1077 final var sql = (
1078 """
1079 UPDATE %s
1080 SET number = ?, uuid = ?, pni = ?, username = ?
1081 WHERE _id = ?
1082 """
1083 ).formatted(TABLE_RECIPIENT);
1084 try (final var statement = connection.prepareStatement(sql)) {
1085 statement.setString(1, address.number().orElse(null));
1086 statement.setBytes(2, address.aci().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
1087 statement.setBytes(3, address.pni().map(PNI::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
1088 statement.setString(4, address.username().orElse(null));
1089 statement.setLong(5, recipientId.id());
1090 statement.executeUpdate();
1091 }
1092 rotateStorageId(connection, recipientId);
1093 }
1094 }
1095
1096 private void deleteRecipient(final Connection connection, final RecipientId recipientId) throws SQLException {
1097 final var sql = (
1098 """
1099 DELETE FROM %s
1100 WHERE _id = ?
1101 """
1102 ).formatted(TABLE_RECIPIENT);
1103 try (final var statement = connection.prepareStatement(sql)) {
1104 statement.setLong(1, recipientId.id());
1105 statement.executeUpdate();
1106 }
1107 }
1108
1109 private void mergeRecipientsLocked(
1110 Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
1111 ) throws SQLException {
1112 final var contact = getContact(connection, recipientId);
1113 if (contact == null) {
1114 final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
1115 storeContact(connection, recipientId, toBeMergedContact);
1116 }
1117
1118 final var profileKey = getProfileKey(connection, recipientId);
1119 if (profileKey == null) {
1120 final var toBeMergedProfileKey = getProfileKey(connection, toBeMergedRecipientId);
1121 storeProfileKey(connection, recipientId, toBeMergedProfileKey, false);
1122 }
1123
1124 final var profileKeyCredential = getExpiringProfileKeyCredential(connection, recipientId);
1125 if (profileKeyCredential == null) {
1126 final var toBeMergedProfileKeyCredential = getExpiringProfileKeyCredential(connection,
1127 toBeMergedRecipientId);
1128 storeExpiringProfileKeyCredential(connection, recipientId, toBeMergedProfileKeyCredential);
1129 }
1130
1131 final var profile = getProfile(connection, recipientId);
1132 if (profile == null) {
1133 final var toBeMergedProfile = getProfile(connection, toBeMergedRecipientId);
1134 storeProfile(connection, recipientId, toBeMergedProfile);
1135 }
1136
1137 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
1138 }
1139
1140 private Optional<RecipientWithAddress> findByNumber(
1141 final Connection connection, final String number
1142 ) throws SQLException {
1143 final var sql = """
1144 SELECT r._id, r.number, r.uuid, r.pni, r.username
1145 FROM %s r
1146 WHERE r.number = ?
1147 LIMIT 1
1148 """.formatted(TABLE_RECIPIENT);
1149 try (final var statement = connection.prepareStatement(sql)) {
1150 statement.setString(1, number);
1151 return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
1152 }
1153 }
1154
1155 private Optional<RecipientWithAddress> findByUsername(
1156 final Connection connection, final String username
1157 ) throws SQLException {
1158 final var sql = """
1159 SELECT r._id, r.number, r.uuid, r.pni, r.username
1160 FROM %s r
1161 WHERE r.username = ?
1162 LIMIT 1
1163 """.formatted(TABLE_RECIPIENT);
1164 try (final var statement = connection.prepareStatement(sql)) {
1165 statement.setString(1, username);
1166 return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
1167 }
1168 }
1169
1170 private Optional<RecipientWithAddress> findByServiceId(
1171 final Connection connection, final ServiceId serviceId
1172 ) throws SQLException {
1173 var recipientWithAddress = Optional.ofNullable(recipientAddressCache.get(serviceId));
1174 if (recipientWithAddress.isPresent()) {
1175 return recipientWithAddress;
1176 }
1177 final var sql = """
1178 SELECT r._id, r.number, r.uuid, r.pni, r.username
1179 FROM %s r
1180 WHERE %s = ?1
1181 LIMIT 1
1182 """.formatted(TABLE_RECIPIENT, serviceId instanceof ACI ? "r.uuid" : "r.pni");
1183 try (final var statement = connection.prepareStatement(sql)) {
1184 statement.setBytes(1, UuidUtil.toByteArray(serviceId.getRawUuid()));
1185 recipientWithAddress = Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
1186 recipientWithAddress.ifPresent(r -> recipientAddressCache.put(serviceId, r));
1187 return recipientWithAddress;
1188 }
1189 }
1190
1191 private Set<RecipientWithAddress> findAllByAddress(
1192 final Connection connection, final RecipientAddress address
1193 ) throws SQLException {
1194 final var sql = """
1195 SELECT r._id, r.number, r.uuid, r.pni, r.username
1196 FROM %s r
1197 WHERE r.uuid = ?1 OR
1198 r.pni = ?2 OR
1199 r.number = ?3 OR
1200 r.username = ?4
1201 """.formatted(TABLE_RECIPIENT);
1202 try (final var statement = connection.prepareStatement(sql)) {
1203 statement.setBytes(1, address.aci().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
1204 statement.setBytes(2, address.pni().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
1205 statement.setString(3, address.number().orElse(null));
1206 statement.setString(4, address.username().orElse(null));
1207 return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet)
1208 .collect(Collectors.toSet());
1209 }
1210 }
1211
1212 private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
1213 final var sql = (
1214 """
1215 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
1216 FROM %s r
1217 WHERE r._id = ? AND (%s)
1218 """
1219 ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
1220 try (final var statement = connection.prepareStatement(sql)) {
1221 statement.setLong(1, recipientId.id());
1222 return Utils.executeQueryForOptional(statement, this::getContactFromResultSet).orElse(null);
1223 }
1224 }
1225
1226 private ProfileKey getProfileKey(final Connection connection, final RecipientId recipientId) throws SQLException {
1227 final var selfRecipientId = resolveRecipientLocked(connection, selfAddressProvider.getSelfAddress());
1228 if (recipientId.equals(selfRecipientId)) {
1229 return selfProfileKeyProvider.getSelfProfileKey();
1230 }
1231 final var sql = (
1232 """
1233 SELECT r.profile_key
1234 FROM %s r
1235 WHERE r._id = ?
1236 """
1237 ).formatted(TABLE_RECIPIENT);
1238 try (final var statement = connection.prepareStatement(sql)) {
1239 statement.setLong(1, recipientId.id());
1240 return Utils.executeQueryForOptional(statement, this::getProfileKeyFromResultSet).orElse(null);
1241 }
1242 }
1243
1244 private ExpiringProfileKeyCredential getExpiringProfileKeyCredential(
1245 final Connection connection, final RecipientId recipientId
1246 ) throws SQLException {
1247 final var sql = (
1248 """
1249 SELECT r.profile_key_credential
1250 FROM %s r
1251 WHERE r._id = ?
1252 """
1253 ).formatted(TABLE_RECIPIENT);
1254 try (final var statement = connection.prepareStatement(sql)) {
1255 statement.setLong(1, recipientId.id());
1256 return Utils.executeQueryForOptional(statement, this::getExpiringProfileKeyCredentialFromResultSet)
1257 .orElse(null);
1258 }
1259 }
1260
1261 public Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException {
1262 final var sql = (
1263 """
1264 SELECT r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities
1265 FROM %s r
1266 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1267 """
1268 ).formatted(TABLE_RECIPIENT);
1269 try (final var statement = connection.prepareStatement(sql)) {
1270 statement.setLong(1, recipientId.id());
1271 return Utils.executeQueryForOptional(statement, this::getProfileFromResultSet).orElse(null);
1272 }
1273 }
1274
1275 private RecipientAddress getRecipientAddressFromResultSet(ResultSet resultSet) throws SQLException {
1276 final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(UuidUtil::parseOrNull).map(PNI::from);
1277 final var serviceIdUuid = Optional.ofNullable(resultSet.getBytes("uuid")).map(UuidUtil::parseOrNull);
1278 final var serviceId = serviceIdUuid.isPresent() && pni.isPresent() && serviceIdUuid.get()
1279 .equals(pni.get().getRawUuid()) ? pni.<ServiceId>map(p -> p) : serviceIdUuid.<ServiceId>map(ACI::from);
1280 final var number = Optional.ofNullable(resultSet.getString("number"));
1281 final var username = Optional.ofNullable(resultSet.getString("username"));
1282 return new RecipientAddress(serviceId, pni, number, username);
1283 }
1284
1285 private RecipientId getRecipientIdFromResultSet(ResultSet resultSet) throws SQLException {
1286 return new RecipientId(resultSet.getLong("_id"), this);
1287 }
1288
1289 private RecipientWithAddress getRecipientWithAddressFromResultSet(final ResultSet resultSet) throws SQLException {
1290 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet),
1291 getRecipientAddressFromResultSet(resultSet));
1292 }
1293
1294 private Recipient getRecipientFromResultSet(final ResultSet resultSet) throws SQLException {
1295 return new Recipient(getRecipientIdFromResultSet(resultSet),
1296 getRecipientAddressFromResultSet(resultSet),
1297 getContactFromResultSet(resultSet),
1298 getProfileKeyFromResultSet(resultSet),
1299 getExpiringProfileKeyCredentialFromResultSet(resultSet),
1300 getProfileFromResultSet(resultSet),
1301 getStorageRecordFromResultSet(resultSet));
1302 }
1303
1304 private Contact getContactFromResultSet(ResultSet resultSet) throws SQLException {
1305 return new Contact(resultSet.getString("given_name"),
1306 resultSet.getString("family_name"),
1307 resultSet.getString("color"),
1308 resultSet.getInt("expiration_time"),
1309 resultSet.getBoolean("blocked"),
1310 resultSet.getBoolean("archived"),
1311 resultSet.getBoolean("profile_sharing"),
1312 resultSet.getBoolean("hidden"));
1313 }
1314
1315 private Profile getProfileFromResultSet(ResultSet resultSet) throws SQLException {
1316 final var profileCapabilities = resultSet.getString("profile_capabilities");
1317 final var profileUnidentifiedAccessMode = resultSet.getString("profile_unidentified_access_mode");
1318 return new Profile(resultSet.getLong("profile_last_update_timestamp"),
1319 resultSet.getString("profile_given_name"),
1320 resultSet.getString("profile_family_name"),
1321 resultSet.getString("profile_about"),
1322 resultSet.getString("profile_about_emoji"),
1323 resultSet.getString("profile_avatar_url_path"),
1324 resultSet.getBytes("profile_mobile_coin_address"),
1325 profileUnidentifiedAccessMode == null
1326 ? Profile.UnidentifiedAccessMode.UNKNOWN
1327 : Profile.UnidentifiedAccessMode.valueOfOrUnknown(profileUnidentifiedAccessMode),
1328 profileCapabilities == null
1329 ? Set.of()
1330 : Arrays.stream(profileCapabilities.split(","))
1331 .map(Profile.Capability::valueOfOrNull)
1332 .filter(Objects::nonNull)
1333 .collect(Collectors.toSet()));
1334 }
1335
1336 private ProfileKey getProfileKeyFromResultSet(ResultSet resultSet) throws SQLException {
1337 final var profileKey = resultSet.getBytes("profile_key");
1338
1339 if (profileKey == null) {
1340 return null;
1341 }
1342 try {
1343 return new ProfileKey(profileKey);
1344 } catch (InvalidInputException ignored) {
1345 return null;
1346 }
1347 }
1348
1349 private ExpiringProfileKeyCredential getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet) throws SQLException {
1350 final var profileKeyCredential = resultSet.getBytes("profile_key_credential");
1351
1352 if (profileKeyCredential == null) {
1353 return null;
1354 }
1355 try {
1356 return new ExpiringProfileKeyCredential(profileKeyCredential);
1357 } catch (Throwable ignored) {
1358 return null;
1359 }
1360 }
1361
1362 private StorageId getContactStorageIdFromResultSet(ResultSet resultSet) throws SQLException {
1363 final var storageId = resultSet.getBytes("storage_id");
1364 return StorageId.forContact(storageId);
1365 }
1366
1367 private byte[] getStorageRecordFromResultSet(ResultSet resultSet) throws SQLException {
1368 return resultSet.getBytes("storage_record");
1369 }
1370
1371 public interface RecipientMergeHandler {
1372
1373 void mergeRecipients(
1374 final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
1375 ) throws SQLException;
1376 }
1377
1378 private class HelperStore implements MergeRecipientHelper.Store {
1379
1380 private final Connection connection;
1381
1382 public HelperStore(final Connection connection) {
1383 this.connection = connection;
1384 }
1385
1386 @Override
1387 public Set<RecipientWithAddress> findAllByAddress(final RecipientAddress address) throws SQLException {
1388 return RecipientStore.this.findAllByAddress(connection, address);
1389 }
1390
1391 @Override
1392 public RecipientId addNewRecipient(final RecipientAddress address) throws SQLException {
1393 return RecipientStore.this.addNewRecipient(connection, address);
1394 }
1395
1396 @Override
1397 public void updateRecipientAddress(
1398 final RecipientId recipientId, final RecipientAddress address
1399 ) throws SQLException {
1400 RecipientStore.this.updateRecipientAddress(connection, recipientId, address);
1401 }
1402
1403 @Override
1404 public void removeRecipientAddress(final RecipientId recipientId) throws SQLException {
1405 RecipientStore.this.removeRecipientAddress(connection, recipientId);
1406 }
1407 }
1408 }