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