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