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