]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
668366fe57b8a2ef142599df7a5b67c9b1d4b7ca
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / recipients / RecipientStore.java
1 package org.asamk.signal.manager.storage.recipients;
2
3 import org.asamk.signal.manager.api.Contact;
4 import org.asamk.signal.manager.api.Pair;
5 import org.asamk.signal.manager.api.Profile;
6 import org.asamk.signal.manager.api.UnregisteredRecipientException;
7 import org.asamk.signal.manager.storage.Database;
8 import org.asamk.signal.manager.storage.Utils;
9 import org.asamk.signal.manager.storage.contacts.ContactsStore;
10 import org.asamk.signal.manager.storage.profiles.ProfileStore;
11 import org.signal.libsignal.zkgroup.InvalidInputException;
12 import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
13 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
14 import org.slf4j.Logger;
15 import org.slf4j.LoggerFactory;
16 import org.whispersystems.signalservice.api.push.ServiceId;
17 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
18 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
19 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
20 import org.whispersystems.signalservice.api.util.UuidUtil;
21
22 import java.sql.Connection;
23 import java.sql.ResultSet;
24 import java.sql.SQLException;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Objects;
32 import java.util.Optional;
33 import java.util.Set;
34 import java.util.function.Supplier;
35 import java.util.stream.Collectors;
36
37 public class RecipientStore implements RecipientIdCreator, RecipientResolver, RecipientTrustedResolver, ContactsStore, ProfileStore {
38
39 private static final Logger logger = LoggerFactory.getLogger(RecipientStore.class);
40 private static final String TABLE_RECIPIENT = "recipient";
41 private static final String SQL_IS_CONTACT = "r.given_name IS NOT NULL OR r.family_name IS NOT NULL OR r.expiration_time > 0 OR r.profile_sharing = TRUE OR r.color IS NOT NULL OR r.blocked = TRUE OR r.archived = TRUE";
42
43 private final RecipientMergeHandler recipientMergeHandler;
44 private final SelfAddressProvider selfAddressProvider;
45 private final Database database;
46
47 private final Object recipientsLock = new Object();
48 private final Map<Long, Long> recipientsMerged = new HashMap<>();
49
50 private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = new HashMap<>();
51
52 public static void createSql(Connection connection) throws SQLException {
53 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
54 try (final var statement = connection.createStatement()) {
55 statement.executeUpdate("""
56 CREATE TABLE recipient (
57 _id INTEGER PRIMARY KEY AUTOINCREMENT,
58 number TEXT UNIQUE,
59 username TEXT UNIQUE,
60 uuid BLOB UNIQUE,
61 pni BLOB UNIQUE,
62 profile_key BLOB,
63 profile_key_credential BLOB,
64
65 given_name TEXT,
66 family_name TEXT,
67 color TEXT,
68
69 expiration_time INTEGER NOT NULL DEFAULT 0,
70 blocked INTEGER NOT NULL DEFAULT FALSE,
71 archived INTEGER NOT NULL DEFAULT FALSE,
72 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
73 hidden INTEGER NOT NULL DEFAULT FALSE,
74
75 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
76 profile_given_name TEXT,
77 profile_family_name TEXT,
78 profile_about TEXT,
79 profile_about_emoji TEXT,
80 profile_avatar_url_path TEXT,
81 profile_mobile_coin_address BLOB,
82 profile_unidentified_access_mode TEXT,
83 profile_capabilities TEXT
84 ) STRICT;
85 """);
86 }
87 }
88
89 public RecipientStore(
90 final RecipientMergeHandler recipientMergeHandler,
91 final SelfAddressProvider selfAddressProvider,
92 final Database database
93 ) {
94 this.recipientMergeHandler = recipientMergeHandler;
95 this.selfAddressProvider = selfAddressProvider;
96 this.database = database;
97 }
98
99 public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
100 final var sql = (
101 """
102 SELECT r.number, r.uuid, r.pni, r.username
103 FROM %s r
104 WHERE r._id = ?
105 """
106 ).formatted(TABLE_RECIPIENT);
107 try (final var connection = database.getConnection()) {
108 try (final var statement = connection.prepareStatement(sql)) {
109 statement.setLong(1, recipientId.id());
110 return Utils.executeQuerySingleRow(statement, this::getRecipientAddressFromResultSet);
111 }
112 } catch (SQLException e) {
113 throw new RuntimeException("Failed read from recipient store", e);
114 }
115 }
116
117 public Collection<RecipientId> getRecipientIdsWithEnabledProfileSharing() {
118 final var sql = (
119 """
120 SELECT r._id
121 FROM %s r
122 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
123 """
124 ).formatted(TABLE_RECIPIENT);
125 try (final var connection = database.getConnection()) {
126 try (final var statement = connection.prepareStatement(sql)) {
127 try (var result = Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet)) {
128 return result.toList();
129 }
130 }
131 } catch (SQLException e) {
132 throw new RuntimeException("Failed read from recipient store", e);
133 }
134 }
135
136 @Override
137 public RecipientId resolveRecipient(final long rawRecipientId) {
138 final var sql = (
139 """
140 SELECT r._id
141 FROM %s r
142 WHERE r._id = ?
143 """
144 ).formatted(TABLE_RECIPIENT);
145 try (final var connection = database.getConnection()) {
146 try (final var statement = connection.prepareStatement(sql)) {
147 statement.setLong(1, rawRecipientId);
148 return Utils.executeQueryForOptional(statement, this::getRecipientIdFromResultSet).orElse(null);
149 }
150 } catch (SQLException e) {
151 throw new RuntimeException("Failed read from recipient store", e);
152 }
153 }
154
155 @Override
156 public RecipientId resolveRecipient(final String identifier) {
157 final var serviceId = ServiceId.parseOrNull(identifier);
158 if (serviceId != null) {
159 return resolveRecipient(serviceId);
160 } else {
161 return resolveRecipientByNumber(identifier);
162 }
163 }
164
165 private RecipientId resolveRecipientByNumber(final String number) {
166 synchronized (recipientsLock) {
167 final RecipientId recipientId;
168 try (final var connection = database.getConnection()) {
169 connection.setAutoCommit(false);
170 recipientId = resolveRecipientLocked(connection, number);
171 connection.commit();
172 } catch (SQLException e) {
173 throw new RuntimeException("Failed read recipient store", e);
174 }
175 return recipientId;
176 }
177 }
178
179 @Override
180 public RecipientId resolveRecipient(final ServiceId serviceId) {
181 synchronized (recipientsLock) {
182 final var recipientWithAddress = recipientAddressCache.get(serviceId);
183 if (recipientWithAddress != null) {
184 return recipientWithAddress.id();
185 }
186 try (final var connection = database.getConnection()) {
187 connection.setAutoCommit(false);
188 final var recipientId = resolveRecipientLocked(connection, serviceId);
189 connection.commit();
190 return recipientId;
191 } catch (SQLException e) {
192 throw new RuntimeException("Failed read recipient store", e);
193 }
194 }
195 }
196
197 /**
198 * Should only be used for recipientIds from the database.
199 * Where the foreign key relations ensure a valid recipientId.
200 */
201 @Override
202 public RecipientId create(final long recipientId) {
203 return new RecipientId(recipientId, this);
204 }
205
206 public RecipientId resolveRecipientByNumber(
207 final String number, Supplier<ServiceId> serviceIdSupplier
208 ) throws UnregisteredRecipientException {
209 final Optional<RecipientWithAddress> byNumber;
210 try (final var connection = database.getConnection()) {
211 byNumber = findByNumber(connection, number);
212 } catch (SQLException e) {
213 throw new RuntimeException("Failed read from recipient store", e);
214 }
215 if (byNumber.isEmpty() || byNumber.get().address().serviceId().isEmpty()) {
216 final var serviceId = serviceIdSupplier.get();
217 if (serviceId == null) {
218 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
219 number));
220 }
221
222 return resolveRecipient(serviceId);
223 }
224 return byNumber.get().id();
225 }
226
227 public Optional<RecipientId> resolveRecipientByNumberOptional(final String number) {
228 final Optional<RecipientWithAddress> byNumber;
229 try (final var connection = database.getConnection()) {
230 byNumber = findByNumber(connection, number);
231 } catch (SQLException e) {
232 throw new RuntimeException("Failed read from recipient store", e);
233 }
234 return byNumber.map(RecipientWithAddress::id);
235 }
236
237 public RecipientId resolveRecipientByUsername(
238 final String username, Supplier<ACI> aciSupplier
239 ) throws UnregisteredRecipientException {
240 final Optional<RecipientWithAddress> byUsername;
241 try (final var connection = database.getConnection()) {
242 byUsername = findByUsername(connection, username);
243 } catch (SQLException e) {
244 throw new RuntimeException("Failed read from recipient store", e);
245 }
246 if (byUsername.isEmpty() || byUsername.get().address().serviceId().isEmpty()) {
247 final var aci = aciSupplier.get();
248 if (aci == null) {
249 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
250 null,
251 username));
252 }
253
254 return resolveRecipientTrusted(aci, username);
255 }
256 return byUsername.get().id();
257 }
258
259 public RecipientId resolveRecipient(RecipientAddress address) {
260 synchronized (recipientsLock) {
261 final RecipientId recipientId;
262 try (final var connection = database.getConnection()) {
263 connection.setAutoCommit(false);
264 recipientId = resolveRecipientLocked(connection, address);
265 connection.commit();
266 } catch (SQLException e) {
267 throw new RuntimeException("Failed read recipient store", e);
268 }
269 return recipientId;
270 }
271 }
272
273 @Override
274 public RecipientId resolveSelfRecipientTrusted(RecipientAddress address) {
275 return resolveRecipientTrusted(address, true);
276 }
277
278 @Override
279 public RecipientId resolveRecipientTrusted(RecipientAddress address) {
280 return resolveRecipientTrusted(address, false);
281 }
282
283 @Override
284 public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
285 return resolveRecipientTrusted(new RecipientAddress(address));
286 }
287
288 @Override
289 public RecipientId resolveRecipientTrusted(
290 final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
291 ) {
292 final var serviceId = aci.map(a -> (ServiceId) a).or(() -> pni);
293 return resolveRecipientTrusted(new RecipientAddress(serviceId, pni, number, Optional.empty()));
294 }
295
296 @Override
297 public RecipientId resolveRecipientTrusted(final ACI aci, final String username) {
298 return resolveRecipientTrusted(new RecipientAddress(aci, null, null, username));
299 }
300
301 @Override
302 public void storeContact(RecipientId recipientId, final Contact contact) {
303 try (final var connection = database.getConnection()) {
304 storeContact(connection, recipientId, contact);
305 } catch (SQLException e) {
306 throw new RuntimeException("Failed update recipient store", e);
307 }
308 }
309
310 @Override
311 public Contact getContact(RecipientId recipientId) {
312 try (final var connection = database.getConnection()) {
313 return getContact(connection, recipientId);
314 } catch (SQLException e) {
315 throw new RuntimeException("Failed read from recipient store", e);
316 }
317 }
318
319 @Override
320 public List<Pair<RecipientId, Contact>> getContacts() {
321 final var sql = (
322 """
323 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
324 FROM %s r
325 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s AND r.hidden = FALSE
326 """
327 ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
328 try (final var connection = database.getConnection()) {
329 try (final var statement = connection.prepareStatement(sql)) {
330 try (var result = Utils.executeQueryForStream(statement,
331 resultSet -> new Pair<>(getRecipientIdFromResultSet(resultSet),
332 getContactFromResultSet(resultSet)))) {
333 return result.toList();
334 }
335 }
336 } catch (SQLException e) {
337 throw new RuntimeException("Failed read from recipient store", e);
338 }
339 }
340
341 public List<Recipient> getRecipients(
342 boolean onlyContacts, Optional<Boolean> blocked, Set<RecipientId> recipientIds, Optional<String> name
343 ) {
344 final var sqlWhere = new ArrayList<String>();
345 if (onlyContacts) {
346 sqlWhere.add("(" + SQL_IS_CONTACT + ")");
347 sqlWhere.add("r.hidden = FALSE");
348 }
349 if (blocked.isPresent()) {
350 sqlWhere.add("r.blocked = ?");
351 }
352 if (!recipientIds.isEmpty()) {
353 final var recipientIdsCommaSeparated = recipientIds.stream()
354 .map(recipientId -> String.valueOf(recipientId.id()))
355 .collect(Collectors.joining(","));
356 sqlWhere.add("r._id IN (" + recipientIdsCommaSeparated + ")");
357 }
358 final var sql = (
359 """
360 SELECT r._id,
361 r.number, r.uuid, r.pni, r.username,
362 r.profile_key, r.profile_key_credential,
363 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
364 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
365 FROM %s r
366 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
367 """
368 ).formatted(TABLE_RECIPIENT, sqlWhere.isEmpty() ? "TRUE" : String.join(" AND ", sqlWhere));
369 try (final var connection = database.getConnection()) {
370 try (final var statement = connection.prepareStatement(sql)) {
371 if (blocked.isPresent()) {
372 statement.setBoolean(1, blocked.get());
373 }
374 try (var result = Utils.executeQueryForStream(statement, this::getRecipientFromResultSet)) {
375 return result.filter(r -> name.isEmpty() || (
376 r.getContact() != null && name.get().equals(r.getContact().getName())
377 ) || (r.getProfile() != null && name.get().equals(r.getProfile().getDisplayName()))).toList();
378 }
379 }
380 } catch (SQLException e) {
381 throw new RuntimeException("Failed read from recipient store", e);
382 }
383 }
384
385 public Set<String> getAllNumbers() {
386 final var sql = (
387 """
388 SELECT r.number
389 FROM %s r
390 WHERE r.number IS NOT NULL
391 """
392 ).formatted(TABLE_RECIPIENT);
393 final var selfNumber = selfAddressProvider.getSelfAddress().number().orElse(null);
394 try (final var connection = database.getConnection()) {
395 try (final var statement = connection.prepareStatement(sql)) {
396 return Utils.executeQueryForStream(statement, resultSet -> resultSet.getString("number"))
397 .filter(Objects::nonNull)
398 .filter(n -> !n.equals(selfNumber))
399 .filter(n -> {
400 try {
401 Long.parseLong(n);
402 return true;
403 } catch (NumberFormatException e) {
404 return false;
405 }
406 })
407 .collect(Collectors.toSet());
408 }
409 } catch (SQLException e) {
410 throw new RuntimeException("Failed read from recipient store", e);
411 }
412 }
413
414 public Map<ServiceId, ProfileKey> getServiceIdToProfileKeyMap() {
415 final var sql = (
416 """
417 SELECT r.uuid, r.profile_key
418 FROM %s r
419 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
420 """
421 ).formatted(TABLE_RECIPIENT);
422 try (final var connection = database.getConnection()) {
423 try (final var statement = connection.prepareStatement(sql)) {
424 return Utils.executeQueryForStream(statement, resultSet -> {
425 final var serviceId = ServiceId.parseOrThrow(resultSet.getBytes("uuid"));
426 final var profileKey = getProfileKeyFromResultSet(resultSet);
427 return new Pair<>(serviceId, profileKey);
428 }).filter(Objects::nonNull).collect(Collectors.toMap(Pair::first, Pair::second));
429 }
430 } catch (SQLException e) {
431 throw new RuntimeException("Failed read from recipient store", e);
432 }
433 }
434
435 @Override
436 public void deleteContact(RecipientId recipientId) {
437 storeContact(recipientId, null);
438 }
439
440 public void deleteRecipientData(RecipientId recipientId) {
441 logger.debug("Deleting recipient data for {}", recipientId);
442 synchronized (recipientsLock) {
443 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
444 try (final var connection = database.getConnection()) {
445 connection.setAutoCommit(false);
446 storeContact(connection, recipientId, null);
447 storeProfile(connection, recipientId, null);
448 storeProfileKey(connection, recipientId, null, false);
449 storeExpiringProfileKeyCredential(connection, recipientId, null);
450 deleteRecipient(connection, recipientId);
451 connection.commit();
452 } catch (SQLException e) {
453 throw new RuntimeException("Failed update recipient store", e);
454 }
455 }
456 }
457
458 @Override
459 public Profile getProfile(final RecipientId recipientId) {
460 try (final var connection = database.getConnection()) {
461 return getProfile(connection, recipientId);
462 } catch (SQLException e) {
463 throw new RuntimeException("Failed read from recipient store", e);
464 }
465 }
466
467 @Override
468 public ProfileKey getProfileKey(final RecipientId recipientId) {
469 try (final var connection = database.getConnection()) {
470 return getProfileKey(connection, recipientId);
471 } catch (SQLException e) {
472 throw new RuntimeException("Failed read from recipient store", e);
473 }
474 }
475
476 @Override
477 public ExpiringProfileKeyCredential getExpiringProfileKeyCredential(final RecipientId recipientId) {
478 try (final var connection = database.getConnection()) {
479 return getExpiringProfileKeyCredential(connection, recipientId);
480 } catch (SQLException e) {
481 throw new RuntimeException("Failed read from recipient store", e);
482 }
483 }
484
485 @Override
486 public void storeProfile(RecipientId recipientId, final Profile profile) {
487 try (final var connection = database.getConnection()) {
488 storeProfile(connection, recipientId, profile);
489 } catch (SQLException e) {
490 throw new RuntimeException("Failed update recipient store", e);
491 }
492 }
493
494 @Override
495 public void storeSelfProfileKey(final RecipientId recipientId, final ProfileKey profileKey) {
496 try (final var connection = database.getConnection()) {
497 storeProfileKey(connection, recipientId, profileKey, false);
498 } catch (SQLException e) {
499 throw new RuntimeException("Failed update recipient store", e);
500 }
501 }
502
503 @Override
504 public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
505 try (final var connection = database.getConnection()) {
506 storeProfileKey(connection, recipientId, profileKey, true);
507 } catch (SQLException e) {
508 throw new RuntimeException("Failed update recipient store", e);
509 }
510 }
511
512 @Override
513 public void storeExpiringProfileKeyCredential(
514 RecipientId recipientId, final ExpiringProfileKeyCredential profileKeyCredential
515 ) {
516 try (final var connection = database.getConnection()) {
517 storeExpiringProfileKeyCredential(connection, recipientId, profileKeyCredential);
518 } catch (SQLException e) {
519 throw new RuntimeException("Failed update recipient store", e);
520 }
521 }
522
523 void addLegacyRecipients(final Map<RecipientId, Recipient> recipients) {
524 logger.debug("Migrating legacy recipients to database");
525 long start = System.nanoTime();
526 final var sql = (
527 """
528 INSERT INTO %s (_id, number, uuid)
529 VALUES (?, ?, ?)
530 """
531 ).formatted(TABLE_RECIPIENT);
532 try (final var connection = database.getConnection()) {
533 connection.setAutoCommit(false);
534 try (final var statement = connection.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT))) {
535 statement.executeUpdate();
536 }
537 try (final var statement = connection.prepareStatement(sql)) {
538 for (final var recipient : recipients.values()) {
539 statement.setLong(1, recipient.getRecipientId().id());
540 statement.setString(2, recipient.getAddress().number().orElse(null));
541 statement.setBytes(3,
542 recipient.getAddress()
543 .serviceId()
544 .map(ServiceId::getRawUuid)
545 .map(UuidUtil::toByteArray)
546 .orElse(null));
547 statement.executeUpdate();
548 }
549 }
550 logger.debug("Initial inserts took {}ms", (System.nanoTime() - start) / 1000000);
551
552 for (final var recipient : recipients.values()) {
553 if (recipient.getContact() != null) {
554 storeContact(connection, recipient.getRecipientId(), recipient.getContact());
555 }
556 if (recipient.getProfile() != null) {
557 storeProfile(connection, recipient.getRecipientId(), recipient.getProfile());
558 }
559 if (recipient.getProfileKey() != null) {
560 storeProfileKey(connection, recipient.getRecipientId(), recipient.getProfileKey(), false);
561 }
562 if (recipient.getExpiringProfileKeyCredential() != null) {
563 storeExpiringProfileKeyCredential(connection,
564 recipient.getRecipientId(),
565 recipient.getExpiringProfileKeyCredential());
566 }
567 }
568 connection.commit();
569 } catch (SQLException e) {
570 throw new RuntimeException("Failed update recipient store", e);
571 }
572 logger.debug("Complete recipients migration took {}ms", (System.nanoTime() - start) / 1000000);
573 }
574
575 long getActualRecipientId(long recipientId) {
576 while (recipientsMerged.containsKey(recipientId)) {
577 final var newRecipientId = recipientsMerged.get(recipientId);
578 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
579 recipientId = newRecipientId;
580 }
581 return recipientId;
582 }
583
584 private void storeContact(
585 final Connection connection, final RecipientId recipientId, final Contact contact
586 ) throws SQLException {
587 final var sql = (
588 """
589 UPDATE %s
590 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
591 WHERE _id = ?
592 """
593 ).formatted(TABLE_RECIPIENT);
594 try (final var statement = connection.prepareStatement(sql)) {
595 statement.setString(1, contact == null ? null : contact.givenName());
596 statement.setString(2, contact == null ? null : contact.familyName());
597 statement.setInt(3, contact == null ? 0 : contact.messageExpirationTime());
598 statement.setBoolean(4, contact != null && contact.isProfileSharingEnabled());
599 statement.setString(5, contact == null ? null : contact.color());
600 statement.setBoolean(6, contact != null && contact.isBlocked());
601 statement.setBoolean(7, contact != null && contact.isArchived());
602 statement.setLong(8, recipientId.id());
603 statement.executeUpdate();
604 }
605 }
606
607 private void storeExpiringProfileKeyCredential(
608 final Connection connection,
609 final RecipientId recipientId,
610 final ExpiringProfileKeyCredential profileKeyCredential
611 ) throws SQLException {
612 final var sql = (
613 """
614 UPDATE %s
615 SET profile_key_credential = ?
616 WHERE _id = ?
617 """
618 ).formatted(TABLE_RECIPIENT);
619 try (final var statement = connection.prepareStatement(sql)) {
620 statement.setBytes(1, profileKeyCredential == null ? null : profileKeyCredential.serialize());
621 statement.setLong(2, recipientId.id());
622 statement.executeUpdate();
623 }
624 }
625
626 private void storeProfile(
627 final Connection connection, final RecipientId recipientId, final Profile profile
628 ) throws SQLException {
629 final var sql = (
630 """
631 UPDATE %s
632 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 = ?
633 WHERE _id = ?
634 """
635 ).formatted(TABLE_RECIPIENT);
636 try (final var statement = connection.prepareStatement(sql)) {
637 statement.setLong(1, profile == null ? 0 : profile.getLastUpdateTimestamp());
638 statement.setString(2, profile == null ? null : profile.getGivenName());
639 statement.setString(3, profile == null ? null : profile.getFamilyName());
640 statement.setString(4, profile == null ? null : profile.getAbout());
641 statement.setString(5, profile == null ? null : profile.getAboutEmoji());
642 statement.setString(6, profile == null ? null : profile.getAvatarUrlPath());
643 statement.setBytes(7, profile == null ? null : profile.getMobileCoinAddress());
644 statement.setString(8, profile == null ? null : profile.getUnidentifiedAccessMode().name());
645 statement.setString(9,
646 profile == null
647 ? null
648 : profile.getCapabilities().stream().map(Enum::name).collect(Collectors.joining(",")));
649 statement.setLong(10, recipientId.id());
650 statement.executeUpdate();
651 }
652 }
653
654 private void storeProfileKey(
655 Connection connection, RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile
656 ) throws SQLException {
657 if (profileKey != null) {
658 final var recipientProfileKey = getProfileKey(recipientId);
659 if (profileKey.equals(recipientProfileKey)) {
660 final var recipientProfile = getProfile(recipientId);
661 if (recipientProfile == null || (
662 recipientProfile.getUnidentifiedAccessMode() != Profile.UnidentifiedAccessMode.UNKNOWN
663 && recipientProfile.getUnidentifiedAccessMode()
664 != Profile.UnidentifiedAccessMode.DISABLED
665 )) {
666 return;
667 }
668 }
669 }
670
671 final var sql = (
672 """
673 UPDATE %s
674 SET profile_key = ?, profile_key_credential = NULL%s
675 WHERE _id = ?
676 """
677 ).formatted(TABLE_RECIPIENT, resetProfile ? ", profile_last_update_timestamp = 0" : "");
678 try (final var statement = connection.prepareStatement(sql)) {
679 statement.setBytes(1, profileKey == null ? null : profileKey.serialize());
680 statement.setLong(2, recipientId.id());
681 statement.executeUpdate();
682 }
683 }
684
685 private RecipientId resolveRecipientTrusted(RecipientAddress address, boolean isSelf) {
686 final Pair<RecipientId, List<RecipientId>> pair;
687 synchronized (recipientsLock) {
688 try (final var connection = database.getConnection()) {
689 connection.setAutoCommit(false);
690 if (address.hasSingleIdentifier() || (
691 !isSelf && selfAddressProvider.getSelfAddress().matches(address)
692 )) {
693 pair = new Pair<>(resolveRecipientLocked(connection, address), List.of());
694 } else {
695 pair = MergeRecipientHelper.resolveRecipientTrustedLocked(new HelperStore(connection), address);
696
697 for (final var toBeMergedRecipientId : pair.second()) {
698 mergeRecipientsLocked(connection, pair.first(), toBeMergedRecipientId);
699 }
700 }
701 connection.commit();
702 } catch (SQLException e) {
703 throw new RuntimeException("Failed update recipient store", e);
704 }
705 }
706
707 if (!pair.second().isEmpty()) {
708 try (final var connection = database.getConnection()) {
709 for (final var toBeMergedRecipientId : pair.second()) {
710 recipientMergeHandler.mergeRecipients(connection, pair.first(), toBeMergedRecipientId);
711 deleteRecipient(connection, toBeMergedRecipientId);
712 synchronized (recipientsLock) {
713 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId));
714 }
715 }
716 } catch (SQLException e) {
717 throw new RuntimeException("Failed update recipient store", e);
718 }
719 }
720 return pair.first();
721 }
722
723 private RecipientId resolveRecipientLocked(
724 Connection connection, RecipientAddress address
725 ) throws SQLException {
726 final var byServiceId = address.serviceId().isEmpty()
727 ? Optional.<RecipientWithAddress>empty()
728 : findByServiceId(connection, address.serviceId().get());
729
730 if (byServiceId.isPresent()) {
731 return byServiceId.get().id();
732 }
733
734 final var byPni = address.pni().isEmpty()
735 ? Optional.<RecipientWithAddress>empty()
736 : findByServiceId(connection, address.pni().get());
737
738 if (byPni.isPresent()) {
739 return byPni.get().id();
740 }
741
742 final var byNumber = address.number().isEmpty()
743 ? Optional.<RecipientWithAddress>empty()
744 : findByNumber(connection, address.number().get());
745
746 if (byNumber.isPresent()) {
747 return byNumber.get().id();
748 }
749
750 logger.debug("Got new recipient, both serviceId and number are unknown");
751
752 if (address.serviceId().isEmpty()) {
753 return addNewRecipient(connection, address);
754 }
755
756 return addNewRecipient(connection, new RecipientAddress(address.serviceId().get()));
757 }
758
759 private RecipientId resolveRecipientLocked(Connection connection, ServiceId serviceId) throws SQLException {
760 final var recipient = findByServiceId(connection, serviceId);
761
762 if (recipient.isEmpty()) {
763 logger.debug("Got new recipient, serviceId is unknown");
764 return addNewRecipient(connection, new RecipientAddress(serviceId));
765 }
766
767 return recipient.get().id();
768 }
769
770 private RecipientId resolveRecipientLocked(Connection connection, String number) throws SQLException {
771 final var recipient = findByNumber(connection, number);
772
773 if (recipient.isEmpty()) {
774 logger.debug("Got new recipient, number is unknown");
775 return addNewRecipient(connection, new RecipientAddress(null, number));
776 }
777
778 return recipient.get().id();
779 }
780
781 private RecipientId addNewRecipient(
782 final Connection connection, final RecipientAddress address
783 ) throws SQLException {
784 final var sql = (
785 """
786 INSERT INTO %s (number, uuid, pni)
787 VALUES (?, ?, ?)
788 RETURNING _id
789 """
790 ).formatted(TABLE_RECIPIENT);
791 try (final var statement = connection.prepareStatement(sql)) {
792 statement.setString(1, address.number().orElse(null));
793 statement.setBytes(2,
794 address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
795 statement.setBytes(3, address.pni().map(PNI::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
796 final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
797 if (generatedKey.isPresent()) {
798 final var recipientId = new RecipientId(generatedKey.get(), this);
799 logger.debug("Added new recipient {} with address {}", recipientId, address);
800 return recipientId;
801 } else {
802 throw new RuntimeException("Failed to add new recipient to database");
803 }
804 }
805 }
806
807 private void removeRecipientAddress(Connection connection, RecipientId recipientId) throws SQLException {
808 synchronized (recipientsLock) {
809 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
810 final var sql = (
811 """
812 UPDATE %s
813 SET number = NULL, uuid = NULL, pni = NULL
814 WHERE _id = ?
815 """
816 ).formatted(TABLE_RECIPIENT);
817 try (final var statement = connection.prepareStatement(sql)) {
818 statement.setLong(1, recipientId.id());
819 statement.executeUpdate();
820 }
821 }
822 }
823
824 private void updateRecipientAddress(
825 Connection connection, RecipientId recipientId, final RecipientAddress address
826 ) throws SQLException {
827 synchronized (recipientsLock) {
828 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
829 final var sql = (
830 """
831 UPDATE %s
832 SET number = ?, uuid = ?, pni = ?, username = ?
833 WHERE _id = ?
834 """
835 ).formatted(TABLE_RECIPIENT);
836 try (final var statement = connection.prepareStatement(sql)) {
837 statement.setString(1, address.number().orElse(null));
838 statement.setBytes(2,
839 address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
840 statement.setBytes(3, address.pni().map(PNI::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
841 statement.setString(4, address.username().orElse(null));
842 statement.setLong(5, recipientId.id());
843 statement.executeUpdate();
844 }
845 }
846 }
847
848 private void deleteRecipient(final Connection connection, final RecipientId recipientId) throws SQLException {
849 final var sql = (
850 """
851 DELETE FROM %s
852 WHERE _id = ?
853 """
854 ).formatted(TABLE_RECIPIENT);
855 try (final var statement = connection.prepareStatement(sql)) {
856 statement.setLong(1, recipientId.id());
857 statement.executeUpdate();
858 }
859 }
860
861 private void mergeRecipientsLocked(
862 Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
863 ) throws SQLException {
864 final var contact = getContact(connection, recipientId);
865 if (contact == null) {
866 final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
867 storeContact(connection, recipientId, toBeMergedContact);
868 }
869
870 final var profileKey = getProfileKey(connection, recipientId);
871 if (profileKey == null) {
872 final var toBeMergedProfileKey = getProfileKey(connection, toBeMergedRecipientId);
873 storeProfileKey(connection, recipientId, toBeMergedProfileKey, false);
874 }
875
876 final var profileKeyCredential = getExpiringProfileKeyCredential(connection, recipientId);
877 if (profileKeyCredential == null) {
878 final var toBeMergedProfileKeyCredential = getExpiringProfileKeyCredential(connection,
879 toBeMergedRecipientId);
880 storeExpiringProfileKeyCredential(connection, recipientId, toBeMergedProfileKeyCredential);
881 }
882
883 final var profile = getProfile(connection, recipientId);
884 if (profile == null) {
885 final var toBeMergedProfile = getProfile(connection, toBeMergedRecipientId);
886 storeProfile(connection, recipientId, toBeMergedProfile);
887 }
888
889 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
890 }
891
892 private Optional<RecipientWithAddress> findByNumber(
893 final Connection connection, final String number
894 ) throws SQLException {
895 final var sql = """
896 SELECT r._id, r.number, r.uuid, r.pni, r.username
897 FROM %s r
898 WHERE r.number = ?
899 LIMIT 1
900 """.formatted(TABLE_RECIPIENT);
901 try (final var statement = connection.prepareStatement(sql)) {
902 statement.setString(1, number);
903 return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
904 }
905 }
906
907 private Optional<RecipientWithAddress> findByUsername(
908 final Connection connection, final String username
909 ) throws SQLException {
910 final var sql = """
911 SELECT r._id, r.number, r.uuid, r.pni, r.username
912 FROM %s r
913 WHERE r.username = ?
914 LIMIT 1
915 """.formatted(TABLE_RECIPIENT);
916 try (final var statement = connection.prepareStatement(sql)) {
917 statement.setString(1, username);
918 return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
919 }
920 }
921
922 private Optional<RecipientWithAddress> findByServiceId(
923 final Connection connection, final ServiceId serviceId
924 ) throws SQLException {
925 var recipientWithAddress = Optional.ofNullable(recipientAddressCache.get(serviceId));
926 if (recipientWithAddress.isPresent()) {
927 return recipientWithAddress;
928 }
929 final var sql = """
930 SELECT r._id, r.number, r.uuid, r.pni, r.username
931 FROM %s r
932 WHERE r.uuid = ?1 OR r.pni = ?1
933 LIMIT 1
934 """.formatted(TABLE_RECIPIENT);
935 try (final var statement = connection.prepareStatement(sql)) {
936 statement.setBytes(1, UuidUtil.toByteArray(serviceId.getRawUuid()));
937 recipientWithAddress = Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
938 recipientWithAddress.ifPresent(r -> recipientAddressCache.put(serviceId, r));
939 return recipientWithAddress;
940 }
941 }
942
943 private Set<RecipientWithAddress> findAllByAddress(
944 final Connection connection, final RecipientAddress address
945 ) throws SQLException {
946 final var sql = """
947 SELECT r._id, r.number, r.uuid, r.pni, r.username
948 FROM %s r
949 WHERE r.uuid = ?1 OR r.pni = ?1 OR
950 r.uuid = ?2 OR r.pni = ?2 OR
951 r.number = ?3 OR
952 r.username = ?4
953 """.formatted(TABLE_RECIPIENT);
954 try (final var statement = connection.prepareStatement(sql)) {
955 statement.setBytes(1,
956 address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
957 statement.setBytes(2, address.pni().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
958 statement.setString(3, address.number().orElse(null));
959 statement.setString(4, address.username().orElse(null));
960 return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet)
961 .collect(Collectors.toSet());
962 }
963 }
964
965 private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
966 final var sql = (
967 """
968 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
969 FROM %s r
970 WHERE r._id = ? AND (%s)
971 """
972 ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
973 try (final var statement = connection.prepareStatement(sql)) {
974 statement.setLong(1, recipientId.id());
975 return Utils.executeQueryForOptional(statement, this::getContactFromResultSet).orElse(null);
976 }
977 }
978
979 private ProfileKey getProfileKey(final Connection connection, final RecipientId recipientId) throws SQLException {
980 final var sql = (
981 """
982 SELECT r.profile_key
983 FROM %s r
984 WHERE r._id = ?
985 """
986 ).formatted(TABLE_RECIPIENT);
987 try (final var statement = connection.prepareStatement(sql)) {
988 statement.setLong(1, recipientId.id());
989 return Utils.executeQueryForOptional(statement, this::getProfileKeyFromResultSet).orElse(null);
990 }
991 }
992
993 private ExpiringProfileKeyCredential getExpiringProfileKeyCredential(
994 final Connection connection, final RecipientId recipientId
995 ) throws SQLException {
996 final var sql = (
997 """
998 SELECT r.profile_key_credential
999 FROM %s r
1000 WHERE r._id = ?
1001 """
1002 ).formatted(TABLE_RECIPIENT);
1003 try (final var statement = connection.prepareStatement(sql)) {
1004 statement.setLong(1, recipientId.id());
1005 return Utils.executeQueryForOptional(statement, this::getExpiringProfileKeyCredentialFromResultSet)
1006 .orElse(null);
1007 }
1008 }
1009
1010 private Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException {
1011 final var sql = (
1012 """
1013 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
1014 FROM %s r
1015 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1016 """
1017 ).formatted(TABLE_RECIPIENT);
1018 try (final var statement = connection.prepareStatement(sql)) {
1019 statement.setLong(1, recipientId.id());
1020 return Utils.executeQueryForOptional(statement, this::getProfileFromResultSet).orElse(null);
1021 }
1022 }
1023
1024 private RecipientAddress getRecipientAddressFromResultSet(ResultSet resultSet) throws SQLException {
1025 final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(UuidUtil::parseOrNull).map(PNI::from);
1026 final var serviceIdUuid = Optional.ofNullable(resultSet.getBytes("uuid")).map(UuidUtil::parseOrNull);
1027 final var serviceId = serviceIdUuid.isPresent() && pni.isPresent() && serviceIdUuid.get()
1028 .equals(pni.get().getRawUuid()) ? pni.<ServiceId>map(p -> p) : serviceIdUuid.<ServiceId>map(ACI::from);
1029 final var number = Optional.ofNullable(resultSet.getString("number"));
1030 final var username = Optional.ofNullable(resultSet.getString("username"));
1031 return new RecipientAddress(serviceId, pni, number, username);
1032 }
1033
1034 private RecipientId getRecipientIdFromResultSet(ResultSet resultSet) throws SQLException {
1035 return new RecipientId(resultSet.getLong("_id"), this);
1036 }
1037
1038 private RecipientWithAddress getRecipientWithAddressFromResultSet(final ResultSet resultSet) throws SQLException {
1039 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet),
1040 getRecipientAddressFromResultSet(resultSet));
1041 }
1042
1043 private Recipient getRecipientFromResultSet(final ResultSet resultSet) throws SQLException {
1044 return new Recipient(getRecipientIdFromResultSet(resultSet),
1045 getRecipientAddressFromResultSet(resultSet),
1046 getContactFromResultSet(resultSet),
1047 getProfileKeyFromResultSet(resultSet),
1048 getExpiringProfileKeyCredentialFromResultSet(resultSet),
1049 getProfileFromResultSet(resultSet));
1050 }
1051
1052 private Contact getContactFromResultSet(ResultSet resultSet) throws SQLException {
1053 return new Contact(resultSet.getString("given_name"),
1054 resultSet.getString("family_name"),
1055 resultSet.getString("color"),
1056 resultSet.getInt("expiration_time"),
1057 resultSet.getBoolean("blocked"),
1058 resultSet.getBoolean("archived"),
1059 resultSet.getBoolean("profile_sharing"),
1060 resultSet.getBoolean("hidden"));
1061 }
1062
1063 private Profile getProfileFromResultSet(ResultSet resultSet) throws SQLException {
1064 final var profileCapabilities = resultSet.getString("profile_capabilities");
1065 final var profileUnidentifiedAccessMode = resultSet.getString("profile_unidentified_access_mode");
1066 return new Profile(resultSet.getLong("profile_last_update_timestamp"),
1067 resultSet.getString("profile_given_name"),
1068 resultSet.getString("profile_family_name"),
1069 resultSet.getString("profile_about"),
1070 resultSet.getString("profile_about_emoji"),
1071 resultSet.getString("profile_avatar_url_path"),
1072 resultSet.getBytes("profile_mobile_coin_address"),
1073 profileUnidentifiedAccessMode == null
1074 ? Profile.UnidentifiedAccessMode.UNKNOWN
1075 : Profile.UnidentifiedAccessMode.valueOfOrUnknown(profileUnidentifiedAccessMode),
1076 profileCapabilities == null
1077 ? Set.of()
1078 : Arrays.stream(profileCapabilities.split(","))
1079 .map(Profile.Capability::valueOfOrNull)
1080 .filter(Objects::nonNull)
1081 .collect(Collectors.toSet()));
1082 }
1083
1084 private ProfileKey getProfileKeyFromResultSet(ResultSet resultSet) throws SQLException {
1085 final var profileKey = resultSet.getBytes("profile_key");
1086
1087 if (profileKey == null) {
1088 return null;
1089 }
1090 try {
1091 return new ProfileKey(profileKey);
1092 } catch (InvalidInputException ignored) {
1093 return null;
1094 }
1095 }
1096
1097 private ExpiringProfileKeyCredential getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet) throws SQLException {
1098 final var profileKeyCredential = resultSet.getBytes("profile_key_credential");
1099
1100 if (profileKeyCredential == null) {
1101 return null;
1102 }
1103 try {
1104 return new ExpiringProfileKeyCredential(profileKeyCredential);
1105 } catch (Throwable ignored) {
1106 return null;
1107 }
1108 }
1109
1110 public interface RecipientMergeHandler {
1111
1112 void mergeRecipients(
1113 final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
1114 ) throws SQLException;
1115 }
1116
1117 private class HelperStore implements MergeRecipientHelper.Store {
1118
1119 private final Connection connection;
1120
1121 public HelperStore(final Connection connection) {
1122 this.connection = connection;
1123 }
1124
1125 @Override
1126 public Set<RecipientWithAddress> findAllByAddress(final RecipientAddress address) throws SQLException {
1127 return RecipientStore.this.findAllByAddress(connection, address);
1128 }
1129
1130 @Override
1131 public RecipientId addNewRecipient(final RecipientAddress address) throws SQLException {
1132 return RecipientStore.this.addNewRecipient(connection, address);
1133 }
1134
1135 @Override
1136 public void updateRecipientAddress(
1137 final RecipientId recipientId, final RecipientAddress address
1138 ) throws SQLException {
1139 RecipientStore.this.updateRecipientAddress(connection, recipientId, address);
1140 }
1141
1142 @Override
1143 public void removeRecipientAddress(final RecipientId recipientId) throws SQLException {
1144 RecipientStore.this.removeRecipientAddress(connection, recipientId);
1145 }
1146 }
1147 }