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