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