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