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