]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
bc7475b74efa0b35e8a1f1485e034a89252a635b
[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 public RecipientId resolveRecipientTrusted(RecipientAddress address) {
279 return resolveRecipientTrusted(address, false);
280 }
281
282 @Override
283 public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
284 return resolveRecipientTrusted(new RecipientAddress(address), false);
285 }
286
287 @Override
288 public RecipientId resolveRecipientTrusted(
289 final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
290 ) {
291 final var serviceId = aci.map(a -> (ServiceId) a).or(() -> pni);
292 return resolveRecipientTrusted(new RecipientAddress(serviceId, pni, number, Optional.empty()), false);
293 }
294
295 @Override
296 public RecipientId resolveRecipientTrusted(final ACI aci, final String username) {
297 return resolveRecipientTrusted(new RecipientAddress(aci, null, null, username), false);
298 }
299
300 @Override
301 public void storeContact(RecipientId recipientId, final Contact contact) {
302 try (final var connection = database.getConnection()) {
303 storeContact(connection, recipientId, contact);
304 } catch (SQLException e) {
305 throw new RuntimeException("Failed update recipient store", e);
306 }
307 }
308
309 @Override
310 public Contact getContact(RecipientId recipientId) {
311 try (final var connection = database.getConnection()) {
312 return getContact(connection, recipientId);
313 } catch (SQLException e) {
314 throw new RuntimeException("Failed read from recipient store", e);
315 }
316 }
317
318 @Override
319 public List<Pair<RecipientId, Contact>> getContacts() {
320 final var sql = (
321 """
322 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
323 FROM %s r
324 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s AND r.hidden = FALSE
325 """
326 ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
327 try (final var connection = database.getConnection()) {
328 try (final var statement = connection.prepareStatement(sql)) {
329 try (var result = Utils.executeQueryForStream(statement,
330 resultSet -> new Pair<>(getRecipientIdFromResultSet(resultSet),
331 getContactFromResultSet(resultSet)))) {
332 return result.toList();
333 }
334 }
335 } catch (SQLException e) {
336 throw new RuntimeException("Failed read from recipient store", e);
337 }
338 }
339
340 public List<Recipient> getRecipients(
341 boolean onlyContacts, Optional<Boolean> blocked, Set<RecipientId> recipientIds, Optional<String> name
342 ) {
343 final var sqlWhere = new ArrayList<String>();
344 if (onlyContacts) {
345 sqlWhere.add("(" + SQL_IS_CONTACT + ")");
346 sqlWhere.add("r.hidden = FALSE");
347 }
348 if (blocked.isPresent()) {
349 sqlWhere.add("r.blocked = ?");
350 }
351 if (!recipientIds.isEmpty()) {
352 final var recipientIdsCommaSeparated = recipientIds.stream()
353 .map(recipientId -> String.valueOf(recipientId.id()))
354 .collect(Collectors.joining(","));
355 sqlWhere.add("r._id IN (" + recipientIdsCommaSeparated + ")");
356 }
357 final var sql = (
358 """
359 SELECT r._id,
360 r.number, r.uuid, r.pni, r.username,
361 r.profile_key, r.profile_key_credential,
362 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
363 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
364 FROM %s r
365 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
366 """
367 ).formatted(TABLE_RECIPIENT, sqlWhere.isEmpty() ? "TRUE" : String.join(" AND ", sqlWhere));
368 try (final var connection = database.getConnection()) {
369 try (final var statement = connection.prepareStatement(sql)) {
370 if (blocked.isPresent()) {
371 statement.setBoolean(1, blocked.get());
372 }
373 try (var result = Utils.executeQueryForStream(statement, this::getRecipientFromResultSet)) {
374 return result.filter(r -> name.isEmpty() || (
375 r.getContact() != null && name.get().equals(r.getContact().getName())
376 ) || (r.getProfile() != null && name.get().equals(r.getProfile().getDisplayName()))).toList();
377 }
378 }
379 } catch (SQLException e) {
380 throw new RuntimeException("Failed read from recipient store", e);
381 }
382 }
383
384 public Set<String> getAllNumbers() {
385 final var sql = (
386 """
387 SELECT r.number
388 FROM %s r
389 WHERE r.number IS NOT NULL
390 """
391 ).formatted(TABLE_RECIPIENT);
392 final var selfNumber = selfAddressProvider.getSelfAddress().number().orElse(null);
393 try (final var connection = database.getConnection()) {
394 try (final var statement = connection.prepareStatement(sql)) {
395 return Utils.executeQueryForStream(statement, resultSet -> resultSet.getString("number"))
396 .filter(Objects::nonNull)
397 .filter(n -> !n.equals(selfNumber))
398 .filter(n -> {
399 try {
400 Long.parseLong(n);
401 return true;
402 } catch (NumberFormatException e) {
403 return false;
404 }
405 })
406 .collect(Collectors.toSet());
407 }
408 } catch (SQLException e) {
409 throw new RuntimeException("Failed read from recipient store", e);
410 }
411 }
412
413 public Map<ServiceId, ProfileKey> getServiceIdToProfileKeyMap() {
414 final var sql = (
415 """
416 SELECT r.uuid, r.profile_key
417 FROM %s r
418 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
419 """
420 ).formatted(TABLE_RECIPIENT);
421 try (final var connection = database.getConnection()) {
422 try (final var statement = connection.prepareStatement(sql)) {
423 return Utils.executeQueryForStream(statement, resultSet -> {
424 final var serviceId = ServiceId.parseOrThrow(resultSet.getBytes("uuid"));
425 final var profileKey = getProfileKeyFromResultSet(resultSet);
426 return new Pair<>(serviceId, profileKey);
427 }).filter(Objects::nonNull).collect(Collectors.toMap(Pair::first, Pair::second));
428 }
429 } catch (SQLException e) {
430 throw new RuntimeException("Failed read from recipient store", e);
431 }
432 }
433
434 @Override
435 public void deleteContact(RecipientId recipientId) {
436 storeContact(recipientId, null);
437 }
438
439 public void deleteRecipientData(RecipientId recipientId) {
440 logger.debug("Deleting recipient data for {}", recipientId);
441 synchronized (recipientsLock) {
442 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
443 try (final var connection = database.getConnection()) {
444 connection.setAutoCommit(false);
445 storeContact(connection, recipientId, null);
446 storeProfile(connection, recipientId, null);
447 storeProfileKey(connection, recipientId, null, false);
448 storeExpiringProfileKeyCredential(connection, recipientId, null);
449 deleteRecipient(connection, recipientId);
450 connection.commit();
451 } catch (SQLException e) {
452 throw new RuntimeException("Failed update recipient store", e);
453 }
454 }
455 }
456
457 @Override
458 public Profile getProfile(final RecipientId recipientId) {
459 try (final var connection = database.getConnection()) {
460 return getProfile(connection, recipientId);
461 } catch (SQLException e) {
462 throw new RuntimeException("Failed read from recipient store", e);
463 }
464 }
465
466 @Override
467 public ProfileKey getProfileKey(final RecipientId recipientId) {
468 try (final var connection = database.getConnection()) {
469 return getProfileKey(connection, recipientId);
470 } catch (SQLException e) {
471 throw new RuntimeException("Failed read from recipient store", e);
472 }
473 }
474
475 @Override
476 public ExpiringProfileKeyCredential getExpiringProfileKeyCredential(final RecipientId recipientId) {
477 try (final var connection = database.getConnection()) {
478 return getExpiringProfileKeyCredential(connection, recipientId);
479 } catch (SQLException e) {
480 throw new RuntimeException("Failed read from recipient store", e);
481 }
482 }
483
484 @Override
485 public void storeProfile(RecipientId recipientId, final Profile profile) {
486 try (final var connection = database.getConnection()) {
487 storeProfile(connection, recipientId, profile);
488 } catch (SQLException e) {
489 throw new RuntimeException("Failed update recipient store", e);
490 }
491 }
492
493 @Override
494 public void storeSelfProfileKey(final RecipientId recipientId, final ProfileKey profileKey) {
495 try (final var connection = database.getConnection()) {
496 storeProfileKey(connection, recipientId, profileKey, false);
497 } catch (SQLException e) {
498 throw new RuntimeException("Failed update recipient store", e);
499 }
500 }
501
502 @Override
503 public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
504 try (final var connection = database.getConnection()) {
505 storeProfileKey(connection, recipientId, profileKey, true);
506 } catch (SQLException e) {
507 throw new RuntimeException("Failed update recipient store", e);
508 }
509 }
510
511 @Override
512 public void storeExpiringProfileKeyCredential(
513 RecipientId recipientId, final ExpiringProfileKeyCredential profileKeyCredential
514 ) {
515 try (final var connection = database.getConnection()) {
516 storeExpiringProfileKeyCredential(connection, recipientId, profileKeyCredential);
517 } catch (SQLException e) {
518 throw new RuntimeException("Failed update recipient store", e);
519 }
520 }
521
522 void addLegacyRecipients(final Map<RecipientId, Recipient> recipients) {
523 logger.debug("Migrating legacy recipients to database");
524 long start = System.nanoTime();
525 final var sql = (
526 """
527 INSERT INTO %s (_id, number, uuid)
528 VALUES (?, ?, ?)
529 """
530 ).formatted(TABLE_RECIPIENT);
531 try (final var connection = database.getConnection()) {
532 connection.setAutoCommit(false);
533 try (final var statement = connection.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT))) {
534 statement.executeUpdate();
535 }
536 try (final var statement = connection.prepareStatement(sql)) {
537 for (final var recipient : recipients.values()) {
538 statement.setLong(1, recipient.getRecipientId().id());
539 statement.setString(2, recipient.getAddress().number().orElse(null));
540 statement.setBytes(3,
541 recipient.getAddress()
542 .serviceId()
543 .map(ServiceId::getRawUuid)
544 .map(UuidUtil::toByteArray)
545 .orElse(null));
546 statement.executeUpdate();
547 }
548 }
549 logger.debug("Initial inserts took {}ms", (System.nanoTime() - start) / 1000000);
550
551 for (final var recipient : recipients.values()) {
552 if (recipient.getContact() != null) {
553 storeContact(connection, recipient.getRecipientId(), recipient.getContact());
554 }
555 if (recipient.getProfile() != null) {
556 storeProfile(connection, recipient.getRecipientId(), recipient.getProfile());
557 }
558 if (recipient.getProfileKey() != null) {
559 storeProfileKey(connection, recipient.getRecipientId(), recipient.getProfileKey(), false);
560 }
561 if (recipient.getExpiringProfileKeyCredential() != null) {
562 storeExpiringProfileKeyCredential(connection,
563 recipient.getRecipientId(),
564 recipient.getExpiringProfileKeyCredential());
565 }
566 }
567 connection.commit();
568 } catch (SQLException e) {
569 throw new RuntimeException("Failed update recipient store", e);
570 }
571 logger.debug("Complete recipients migration took {}ms", (System.nanoTime() - start) / 1000000);
572 }
573
574 long getActualRecipientId(long recipientId) {
575 while (recipientsMerged.containsKey(recipientId)) {
576 final var newRecipientId = recipientsMerged.get(recipientId);
577 logger.debug("Using {} instead of {}, because recipients have been merged", newRecipientId, recipientId);
578 recipientId = newRecipientId;
579 }
580 return recipientId;
581 }
582
583 private void storeContact(
584 final Connection connection, final RecipientId recipientId, final Contact contact
585 ) throws SQLException {
586 final var sql = (
587 """
588 UPDATE %s
589 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
590 WHERE _id = ?
591 """
592 ).formatted(TABLE_RECIPIENT);
593 try (final var statement = connection.prepareStatement(sql)) {
594 statement.setString(1, contact == null ? null : contact.givenName());
595 statement.setString(2, contact == null ? null : contact.familyName());
596 statement.setInt(3, contact == null ? 0 : contact.messageExpirationTime());
597 statement.setBoolean(4, contact != null && contact.isProfileSharingEnabled());
598 statement.setString(5, contact == null ? null : contact.color());
599 statement.setBoolean(6, contact != null && contact.isBlocked());
600 statement.setBoolean(7, contact != null && contact.isArchived());
601 statement.setLong(8, recipientId.id());
602 statement.executeUpdate();
603 }
604 }
605
606 private void storeExpiringProfileKeyCredential(
607 final Connection connection,
608 final RecipientId recipientId,
609 final ExpiringProfileKeyCredential profileKeyCredential
610 ) throws SQLException {
611 final var sql = (
612 """
613 UPDATE %s
614 SET profile_key_credential = ?
615 WHERE _id = ?
616 """
617 ).formatted(TABLE_RECIPIENT);
618 try (final var statement = connection.prepareStatement(sql)) {
619 statement.setBytes(1, profileKeyCredential == null ? null : profileKeyCredential.serialize());
620 statement.setLong(2, recipientId.id());
621 statement.executeUpdate();
622 }
623 }
624
625 private void storeProfile(
626 final Connection connection, final RecipientId recipientId, final Profile profile
627 ) throws SQLException {
628 final var sql = (
629 """
630 UPDATE %s
631 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 = ?
632 WHERE _id = ?
633 """
634 ).formatted(TABLE_RECIPIENT);
635 try (final var statement = connection.prepareStatement(sql)) {
636 statement.setLong(1, profile == null ? 0 : profile.getLastUpdateTimestamp());
637 statement.setString(2, profile == null ? null : profile.getGivenName());
638 statement.setString(3, profile == null ? null : profile.getFamilyName());
639 statement.setString(4, profile == null ? null : profile.getAbout());
640 statement.setString(5, profile == null ? null : profile.getAboutEmoji());
641 statement.setString(6, profile == null ? null : profile.getAvatarUrlPath());
642 statement.setBytes(7, profile == null ? null : profile.getMobileCoinAddress());
643 statement.setString(8, profile == null ? null : profile.getUnidentifiedAccessMode().name());
644 statement.setString(9,
645 profile == null
646 ? null
647 : profile.getCapabilities().stream().map(Enum::name).collect(Collectors.joining(",")));
648 statement.setLong(10, recipientId.id());
649 statement.executeUpdate();
650 }
651 }
652
653 private void storeProfileKey(
654 Connection connection, RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile
655 ) throws SQLException {
656 if (profileKey != null) {
657 final var recipientProfileKey = getProfileKey(recipientId);
658 if (profileKey.equals(recipientProfileKey)) {
659 final var recipientProfile = getProfile(recipientId);
660 if (recipientProfile == null || (
661 recipientProfile.getUnidentifiedAccessMode() != Profile.UnidentifiedAccessMode.UNKNOWN
662 && recipientProfile.getUnidentifiedAccessMode()
663 != Profile.UnidentifiedAccessMode.DISABLED
664 )) {
665 return;
666 }
667 }
668 }
669
670 final var sql = (
671 """
672 UPDATE %s
673 SET profile_key = ?, profile_key_credential = NULL%s
674 WHERE _id = ?
675 """
676 ).formatted(TABLE_RECIPIENT, resetProfile ? ", profile_last_update_timestamp = 0" : "");
677 try (final var statement = connection.prepareStatement(sql)) {
678 statement.setBytes(1, profileKey == null ? null : profileKey.serialize());
679 statement.setLong(2, recipientId.id());
680 statement.executeUpdate();
681 }
682 }
683
684 private RecipientId resolveRecipientTrusted(RecipientAddress address, boolean isSelf) {
685 final Pair<RecipientId, List<RecipientId>> pair;
686 synchronized (recipientsLock) {
687 try (final var connection = database.getConnection()) {
688 connection.setAutoCommit(false);
689 if (address.hasSingleIdentifier() || (
690 !isSelf && selfAddressProvider.getSelfAddress().matches(address)
691 )) {
692 pair = new Pair<>(resolveRecipientLocked(connection, address), List.of());
693 } else {
694 pair = MergeRecipientHelper.resolveRecipientTrustedLocked(new HelperStore(connection), address);
695
696 for (final var toBeMergedRecipientId : pair.second()) {
697 mergeRecipientsLocked(connection, pair.first(), toBeMergedRecipientId);
698 }
699 }
700 connection.commit();
701 } catch (SQLException e) {
702 throw new RuntimeException("Failed update recipient store", e);
703 }
704 }
705
706 if (!pair.second().isEmpty()) {
707 try (final var connection = database.getConnection()) {
708 for (final var toBeMergedRecipientId : pair.second()) {
709 recipientMergeHandler.mergeRecipients(connection, pair.first(), toBeMergedRecipientId);
710 deleteRecipient(connection, toBeMergedRecipientId);
711 synchronized (recipientsLock) {
712 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId));
713 }
714 }
715 } catch (SQLException e) {
716 throw new RuntimeException("Failed update recipient store", e);
717 }
718 }
719 return pair.first();
720 }
721
722 private RecipientId resolveRecipientLocked(
723 Connection connection, RecipientAddress address
724 ) throws SQLException {
725 final var byServiceId = address.serviceId().isEmpty()
726 ? Optional.<RecipientWithAddress>empty()
727 : findByServiceId(connection, address.serviceId().get());
728
729 if (byServiceId.isPresent()) {
730 return byServiceId.get().id();
731 }
732
733 final var byPni = address.pni().isEmpty()
734 ? Optional.<RecipientWithAddress>empty()
735 : findByServiceId(connection, address.pni().get());
736
737 if (byPni.isPresent()) {
738 return byPni.get().id();
739 }
740
741 final var byNumber = address.number().isEmpty()
742 ? Optional.<RecipientWithAddress>empty()
743 : findByNumber(connection, address.number().get());
744
745 if (byNumber.isPresent()) {
746 return byNumber.get().id();
747 }
748
749 logger.debug("Got new recipient, both serviceId and number are unknown");
750
751 if (address.serviceId().isEmpty()) {
752 return addNewRecipient(connection, address);
753 }
754
755 return addNewRecipient(connection, new RecipientAddress(address.serviceId().get()));
756 }
757
758 private RecipientId resolveRecipientLocked(Connection connection, ServiceId serviceId) throws SQLException {
759 final var recipient = findByServiceId(connection, serviceId);
760
761 if (recipient.isEmpty()) {
762 logger.debug("Got new recipient, serviceId is unknown");
763 return addNewRecipient(connection, new RecipientAddress(serviceId));
764 }
765
766 return recipient.get().id();
767 }
768
769 private RecipientId resolveRecipientLocked(Connection connection, String number) throws SQLException {
770 final var recipient = findByNumber(connection, number);
771
772 if (recipient.isEmpty()) {
773 logger.debug("Got new recipient, number is unknown");
774 return addNewRecipient(connection, new RecipientAddress(null, number));
775 }
776
777 return recipient.get().id();
778 }
779
780 private RecipientId addNewRecipient(
781 final Connection connection, final RecipientAddress address
782 ) throws SQLException {
783 final var sql = (
784 """
785 INSERT INTO %s (number, uuid, pni)
786 VALUES (?, ?, ?)
787 RETURNING _id
788 """
789 ).formatted(TABLE_RECIPIENT);
790 try (final var statement = connection.prepareStatement(sql)) {
791 statement.setString(1, address.number().orElse(null));
792 statement.setBytes(2,
793 address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
794 statement.setBytes(3, address.pni().map(PNI::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
795 final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
796 if (generatedKey.isPresent()) {
797 final var recipientId = new RecipientId(generatedKey.get(), this);
798 logger.debug("Added new recipient {} with address {}", recipientId, address);
799 return recipientId;
800 } else {
801 throw new RuntimeException("Failed to add new recipient to database");
802 }
803 }
804 }
805
806 private void removeRecipientAddress(Connection connection, RecipientId recipientId) throws SQLException {
807 synchronized (recipientsLock) {
808 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
809 final var sql = (
810 """
811 UPDATE %s
812 SET number = NULL, uuid = NULL, pni = NULL
813 WHERE _id = ?
814 """
815 ).formatted(TABLE_RECIPIENT);
816 try (final var statement = connection.prepareStatement(sql)) {
817 statement.setLong(1, recipientId.id());
818 statement.executeUpdate();
819 }
820 }
821 }
822
823 private void updateRecipientAddress(
824 Connection connection, RecipientId recipientId, final RecipientAddress address
825 ) throws SQLException {
826 synchronized (recipientsLock) {
827 recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
828 final var sql = (
829 """
830 UPDATE %s
831 SET number = ?, uuid = ?, pni = ?, username = ?
832 WHERE _id = ?
833 """
834 ).formatted(TABLE_RECIPIENT);
835 try (final var statement = connection.prepareStatement(sql)) {
836 statement.setString(1, address.number().orElse(null));
837 statement.setBytes(2,
838 address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
839 statement.setBytes(3, address.pni().map(PNI::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
840 statement.setString(4, address.username().orElse(null));
841 statement.setLong(5, recipientId.id());
842 statement.executeUpdate();
843 }
844 }
845 }
846
847 private void deleteRecipient(final Connection connection, final RecipientId recipientId) throws SQLException {
848 final var sql = (
849 """
850 DELETE FROM %s
851 WHERE _id = ?
852 """
853 ).formatted(TABLE_RECIPIENT);
854 try (final var statement = connection.prepareStatement(sql)) {
855 statement.setLong(1, recipientId.id());
856 statement.executeUpdate();
857 }
858 }
859
860 private void mergeRecipientsLocked(
861 Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
862 ) throws SQLException {
863 final var contact = getContact(connection, recipientId);
864 if (contact == null) {
865 final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
866 storeContact(connection, recipientId, toBeMergedContact);
867 }
868
869 final var profileKey = getProfileKey(connection, recipientId);
870 if (profileKey == null) {
871 final var toBeMergedProfileKey = getProfileKey(connection, toBeMergedRecipientId);
872 storeProfileKey(connection, recipientId, toBeMergedProfileKey, false);
873 }
874
875 final var profileKeyCredential = getExpiringProfileKeyCredential(connection, recipientId);
876 if (profileKeyCredential == null) {
877 final var toBeMergedProfileKeyCredential = getExpiringProfileKeyCredential(connection,
878 toBeMergedRecipientId);
879 storeExpiringProfileKeyCredential(connection, recipientId, toBeMergedProfileKeyCredential);
880 }
881
882 final var profile = getProfile(connection, recipientId);
883 if (profile == null) {
884 final var toBeMergedProfile = getProfile(connection, toBeMergedRecipientId);
885 storeProfile(connection, recipientId, toBeMergedProfile);
886 }
887
888 recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
889 }
890
891 private Optional<RecipientWithAddress> findByNumber(
892 final Connection connection, final String number
893 ) throws SQLException {
894 final var sql = """
895 SELECT r._id, r.number, r.uuid, r.pni, r.username
896 FROM %s r
897 WHERE r.number = ?
898 LIMIT 1
899 """.formatted(TABLE_RECIPIENT);
900 try (final var statement = connection.prepareStatement(sql)) {
901 statement.setString(1, number);
902 return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
903 }
904 }
905
906 private Optional<RecipientWithAddress> findByUsername(
907 final Connection connection, final String username
908 ) throws SQLException {
909 final var sql = """
910 SELECT r._id, r.number, r.uuid, r.pni, r.username
911 FROM %s r
912 WHERE r.username = ?
913 LIMIT 1
914 """.formatted(TABLE_RECIPIENT);
915 try (final var statement = connection.prepareStatement(sql)) {
916 statement.setString(1, username);
917 return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
918 }
919 }
920
921 private Optional<RecipientWithAddress> findByServiceId(
922 final Connection connection, final ServiceId serviceId
923 ) throws SQLException {
924 var recipientWithAddress = Optional.ofNullable(recipientAddressCache.get(serviceId));
925 if (recipientWithAddress.isPresent()) {
926 return recipientWithAddress;
927 }
928 final var sql = """
929 SELECT r._id, r.number, r.uuid, r.pni, r.username
930 FROM %s r
931 WHERE r.uuid = ?1 OR r.pni = ?1
932 LIMIT 1
933 """.formatted(TABLE_RECIPIENT);
934 try (final var statement = connection.prepareStatement(sql)) {
935 statement.setBytes(1, UuidUtil.toByteArray(serviceId.getRawUuid()));
936 recipientWithAddress = Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
937 recipientWithAddress.ifPresent(r -> recipientAddressCache.put(serviceId, r));
938 return recipientWithAddress;
939 }
940 }
941
942 private Set<RecipientWithAddress> findAllByAddress(
943 final Connection connection, final RecipientAddress address
944 ) throws SQLException {
945 final var sql = """
946 SELECT r._id, r.number, r.uuid, r.pni, r.username
947 FROM %s r
948 WHERE r.uuid = ?1 OR r.pni = ?1 OR
949 r.uuid = ?2 OR r.pni = ?2 OR
950 r.number = ?3 OR
951 r.username = ?4
952 """.formatted(TABLE_RECIPIENT);
953 try (final var statement = connection.prepareStatement(sql)) {
954 statement.setBytes(1,
955 address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
956 statement.setBytes(2, address.pni().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null));
957 statement.setString(3, address.number().orElse(null));
958 statement.setString(4, address.username().orElse(null));
959 return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet)
960 .collect(Collectors.toSet());
961 }
962 }
963
964 private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
965 final var sql = (
966 """
967 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
968 FROM %s r
969 WHERE r._id = ? AND (%s)
970 """
971 ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
972 try (final var statement = connection.prepareStatement(sql)) {
973 statement.setLong(1, recipientId.id());
974 return Utils.executeQueryForOptional(statement, this::getContactFromResultSet).orElse(null);
975 }
976 }
977
978 private ProfileKey getProfileKey(final Connection connection, final RecipientId recipientId) throws SQLException {
979 final var sql = (
980 """
981 SELECT r.profile_key
982 FROM %s r
983 WHERE r._id = ?
984 """
985 ).formatted(TABLE_RECIPIENT);
986 try (final var statement = connection.prepareStatement(sql)) {
987 statement.setLong(1, recipientId.id());
988 return Utils.executeQueryForOptional(statement, this::getProfileKeyFromResultSet).orElse(null);
989 }
990 }
991
992 private ExpiringProfileKeyCredential getExpiringProfileKeyCredential(
993 final Connection connection, final RecipientId recipientId
994 ) throws SQLException {
995 final var sql = (
996 """
997 SELECT r.profile_key_credential
998 FROM %s r
999 WHERE r._id = ?
1000 """
1001 ).formatted(TABLE_RECIPIENT);
1002 try (final var statement = connection.prepareStatement(sql)) {
1003 statement.setLong(1, recipientId.id());
1004 return Utils.executeQueryForOptional(statement, this::getExpiringProfileKeyCredentialFromResultSet)
1005 .orElse(null);
1006 }
1007 }
1008
1009 private Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException {
1010 final var sql = (
1011 """
1012 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
1013 FROM %s r
1014 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1015 """
1016 ).formatted(TABLE_RECIPIENT);
1017 try (final var statement = connection.prepareStatement(sql)) {
1018 statement.setLong(1, recipientId.id());
1019 return Utils.executeQueryForOptional(statement, this::getProfileFromResultSet).orElse(null);
1020 }
1021 }
1022
1023 private RecipientAddress getRecipientAddressFromResultSet(ResultSet resultSet) throws SQLException {
1024 final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(UuidUtil::parseOrNull).map(PNI::from);
1025 final var serviceIdUuid = Optional.ofNullable(resultSet.getBytes("uuid")).map(UuidUtil::parseOrNull);
1026 final var serviceId = serviceIdUuid.isPresent() && pni.isPresent() && serviceIdUuid.get()
1027 .equals(pni.get().getRawUuid()) ? pni.<ServiceId>map(p -> p) : serviceIdUuid.<ServiceId>map(ACI::from);
1028 final var number = Optional.ofNullable(resultSet.getString("number"));
1029 final var username = Optional.ofNullable(resultSet.getString("username"));
1030 return new RecipientAddress(serviceId, pni, number, username);
1031 }
1032
1033 private RecipientId getRecipientIdFromResultSet(ResultSet resultSet) throws SQLException {
1034 return new RecipientId(resultSet.getLong("_id"), this);
1035 }
1036
1037 private RecipientWithAddress getRecipientWithAddressFromResultSet(final ResultSet resultSet) throws SQLException {
1038 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet),
1039 getRecipientAddressFromResultSet(resultSet));
1040 }
1041
1042 private Recipient getRecipientFromResultSet(final ResultSet resultSet) throws SQLException {
1043 return new Recipient(getRecipientIdFromResultSet(resultSet),
1044 getRecipientAddressFromResultSet(resultSet),
1045 getContactFromResultSet(resultSet),
1046 getProfileKeyFromResultSet(resultSet),
1047 getExpiringProfileKeyCredentialFromResultSet(resultSet),
1048 getProfileFromResultSet(resultSet));
1049 }
1050
1051 private Contact getContactFromResultSet(ResultSet resultSet) throws SQLException {
1052 return new Contact(resultSet.getString("given_name"),
1053 resultSet.getString("family_name"),
1054 resultSet.getString("color"),
1055 resultSet.getInt("expiration_time"),
1056 resultSet.getBoolean("blocked"),
1057 resultSet.getBoolean("archived"),
1058 resultSet.getBoolean("profile_sharing"),
1059 resultSet.getBoolean("hidden"));
1060 }
1061
1062 private Profile getProfileFromResultSet(ResultSet resultSet) throws SQLException {
1063 final var profileCapabilities = resultSet.getString("profile_capabilities");
1064 final var profileUnidentifiedAccessMode = resultSet.getString("profile_unidentified_access_mode");
1065 return new Profile(resultSet.getLong("profile_last_update_timestamp"),
1066 resultSet.getString("profile_given_name"),
1067 resultSet.getString("profile_family_name"),
1068 resultSet.getString("profile_about"),
1069 resultSet.getString("profile_about_emoji"),
1070 resultSet.getString("profile_avatar_url_path"),
1071 resultSet.getBytes("profile_mobile_coin_address"),
1072 profileUnidentifiedAccessMode == null
1073 ? Profile.UnidentifiedAccessMode.UNKNOWN
1074 : Profile.UnidentifiedAccessMode.valueOfOrUnknown(profileUnidentifiedAccessMode),
1075 profileCapabilities == null
1076 ? Set.of()
1077 : Arrays.stream(profileCapabilities.split(","))
1078 .map(Profile.Capability::valueOfOrNull)
1079 .filter(Objects::nonNull)
1080 .collect(Collectors.toSet()));
1081 }
1082
1083 private ProfileKey getProfileKeyFromResultSet(ResultSet resultSet) throws SQLException {
1084 final var profileKey = resultSet.getBytes("profile_key");
1085
1086 if (profileKey == null) {
1087 return null;
1088 }
1089 try {
1090 return new ProfileKey(profileKey);
1091 } catch (InvalidInputException ignored) {
1092 return null;
1093 }
1094 }
1095
1096 private ExpiringProfileKeyCredential getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet) throws SQLException {
1097 final var profileKeyCredential = resultSet.getBytes("profile_key_credential");
1098
1099 if (profileKeyCredential == null) {
1100 return null;
1101 }
1102 try {
1103 return new ExpiringProfileKeyCredential(profileKeyCredential);
1104 } catch (Throwable ignored) {
1105 return null;
1106 }
1107 }
1108
1109 public interface RecipientMergeHandler {
1110
1111 void mergeRecipients(
1112 final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
1113 ) throws SQLException;
1114 }
1115
1116 private class HelperStore implements MergeRecipientHelper.Store {
1117
1118 private final Connection connection;
1119
1120 public HelperStore(final Connection connection) {
1121 this.connection = connection;
1122 }
1123
1124 @Override
1125 public Set<RecipientWithAddress> findAllByAddress(final RecipientAddress address) throws SQLException {
1126 return RecipientStore.this.findAllByAddress(connection, address);
1127 }
1128
1129 @Override
1130 public RecipientId addNewRecipient(final RecipientAddress address) throws SQLException {
1131 return RecipientStore.this.addNewRecipient(connection, address);
1132 }
1133
1134 @Override
1135 public void updateRecipientAddress(
1136 final RecipientId recipientId, final RecipientAddress address
1137 ) throws SQLException {
1138 RecipientStore.this.updateRecipientAddress(connection, recipientId, address);
1139 }
1140
1141 @Override
1142 public void removeRecipientAddress(final RecipientId recipientId) throws SQLException {
1143 RecipientStore.this.removeRecipientAddress(connection, recipientId);
1144 }
1145 }
1146 }