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