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