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