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