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