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