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