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