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