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