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