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