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