1 package org
.asamk
.signal
.manager
.storage
.recipients
;
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
;
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
;
29 import java
.util
.Objects
;
30 import java
.util
.Optional
;
32 import java
.util
.function
.Supplier
;
33 import java
.util
.stream
.Collectors
;
35 public class RecipientStore
implements RecipientIdCreator
, RecipientResolver
, RecipientTrustedResolver
, ContactsStore
, ProfileStore
{
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";
41 private final RecipientMergeHandler recipientMergeHandler
;
42 private final SelfAddressProvider selfAddressProvider
;
43 private final Database database
;
45 private final Object recipientsLock
= new Object();
46 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
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,
59 profile_key_credential BLOB,
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,
70 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
71 profile_given_name TEXT,
72 profile_family_name 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
84 public RecipientStore(
85 final RecipientMergeHandler recipientMergeHandler
,
86 final SelfAddressProvider selfAddressProvider
,
87 final Database database
89 this.recipientMergeHandler
= recipientMergeHandler
;
90 this.selfAddressProvider
= selfAddressProvider
;
91 this.database
= database
;
94 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
97 SELECT r.number, r.uuid, r.pni, r.username
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
);
107 } catch (SQLException e
) {
108 throw new RuntimeException("Failed read from recipient store", e
);
112 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
117 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
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();
126 } catch (SQLException e
) {
127 throw new RuntimeException("Failed read from recipient store", e
);
132 public RecipientId
resolveRecipient(final long rawRecipientId
) {
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);
145 } catch (SQLException e
) {
146 throw new RuntimeException("Failed read from recipient store", e
);
151 public RecipientId
resolveRecipient(final String identifier
) {
152 if (UuidUtil
.isUuid(identifier
)) {
153 return resolveRecipient(ServiceId
.parseOrThrow(identifier
));
155 return resolveRecipientByNumber(identifier
);
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
);
166 } catch (SQLException e
) {
167 throw new RuntimeException("Failed read recipient store", e
);
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
);
181 } catch (SQLException e
) {
182 throw new RuntimeException("Failed read recipient store", e
);
189 * Should only be used for recipientIds from the database.
190 * Where the foreign key relations ensure a valid recipientId.
193 public RecipientId
create(final long recipientId
) {
194 return new RecipientId(recipientId
, this);
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
);
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,
213 return resolveRecipient(serviceId
);
215 return byNumber
.get().id();
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
);
225 return byNumber
.map(RecipientWithAddress
::id
);
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
);
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,
245 return resolveRecipient(serviceId
);
247 return byUsername
.get().id();
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
);
257 } catch (SQLException e
) {
258 throw new RuntimeException("Failed read recipient store", e
);
265 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
266 return resolveRecipientTrusted(address
, true);
269 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
270 return resolveRecipientTrusted(address
, false);
274 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
275 return resolveRecipientTrusted(new RecipientAddress(address
), false);
279 public RecipientId
resolveRecipientTrusted(
280 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
282 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
283 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()), false);
287 public RecipientId
resolveRecipientTrusted(final ServiceId serviceId
, final String username
) {
288 return resolveRecipientTrusted(new RecipientAddress(serviceId
, null, null, username
), false);
291 public RecipientId
resolveRecipientTrusted(
292 final ACI aci
, final String username
294 return resolveRecipientTrusted(new RecipientAddress(Optional
.of(aci
),
297 Optional
.of(username
)), false);
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
);
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
);
319 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
322 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
324 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
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();
335 } catch (SQLException e
) {
336 throw new RuntimeException("Failed read from recipient store", e
);
340 public List
<Recipient
> getRecipients(
341 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
343 final var sqlWhere
= new ArrayList
<String
>();
345 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
347 if (blocked
.isPresent()) {
348 sqlWhere
.add("r.blocked = ?");
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
+ ")");
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
364 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
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());
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();
378 } catch (SQLException e
) {
379 throw new RuntimeException("Failed read from recipient store", e
);
383 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
386 SELECT r.uuid, r.profile_key
388 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
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
));
399 } catch (SQLException e
) {
400 throw new RuntimeException("Failed read from recipient store", e
);
405 public void deleteContact(RecipientId recipientId
) {
406 storeContact(recipientId
, null);
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
);
419 } catch (SQLException e
) {
420 throw new RuntimeException("Failed update recipient store", e
);
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
);
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
);
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
);
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
);
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
);
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
);
479 public void storeExpiringProfileKeyCredential(
480 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
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
);
489 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
490 logger
.debug("Migrating legacy recipients to database");
491 long start
= System
.nanoTime();
494 INSERT INTO %s (_id, number, uuid)
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();
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()
510 .map(ServiceId
::uuid
)
511 .map(UuidUtil
::toByteArray
)
513 statement
.executeUpdate();
516 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
518 for (final var recipient
: recipients
.values()) {
519 if (recipient
.getContact() != null) {
520 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
522 if (recipient
.getProfile() != null) {
523 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
525 if (recipient
.getProfileKey() != null) {
526 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
528 if (recipient
.getExpiringProfileKeyCredential() != null) {
529 storeExpiringProfileKeyCredential(connection
,
530 recipient
.getRecipientId(),
531 recipient
.getExpiringProfileKeyCredential());
535 } catch (SQLException e
) {
536 throw new RuntimeException("Failed update recipient store", e
);
538 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
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
;
550 private void storeContact(
551 final Connection connection
, final RecipientId recipientId
, final Contact contact
552 ) throws SQLException
{
556 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
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();
573 private void storeExpiringProfileKeyCredential(
574 final Connection connection
,
575 final RecipientId recipientId
,
576 final ExpiringProfileKeyCredential profileKeyCredential
577 ) throws SQLException
{
581 SET profile_key_credential = ?
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();
592 private void storeProfile(
593 final Connection connection
, final RecipientId recipientId
, final Profile profile
594 ) throws SQLException
{
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 = ?
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,
614 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
615 statement
.setLong(10, recipientId
.id());
616 statement
.executeUpdate();
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
640 SET profile_key = ?, profile_key_credential = NULL%s
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();
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
)
659 pair
= new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
661 pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
663 for (final var toBeMergedRecipientId
: pair
.second()) {
664 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
668 } catch (SQLException e
) {
669 throw new RuntimeException("Failed update recipient store", e
);
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
);
679 } catch (SQLException e
) {
680 throw new RuntimeException("Failed update recipient store", e
);
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());
693 if (byServiceId
.isPresent()) {
694 return byServiceId
.get().id();
697 final var byPni
= address
.pni().isEmpty()
698 ? Optional
.<RecipientWithAddress
>empty()
699 : findByServiceId(connection
, address
.pni().get());
701 if (byPni
.isPresent()) {
702 return byPni
.get().id();
705 final var byNumber
= address
.number().isEmpty()
706 ? Optional
.<RecipientWithAddress
>empty()
707 : findByNumber(connection
, address
.number().get());
709 if (byNumber
.isPresent()) {
710 return byNumber
.get().id();
713 logger
.debug("Got new recipient, both serviceId and number are unknown");
715 if (address
.serviceId().isEmpty()) {
716 return addNewRecipient(connection
, address
);
719 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
722 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
723 final var recipient
= findByServiceId(connection
, serviceId
);
725 if (recipient
.isEmpty()) {
726 logger
.debug("Got new recipient, serviceId is unknown");
727 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
730 return recipient
.get().id();
733 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
734 final var recipient
= findByNumber(connection
, number
);
736 if (recipient
.isEmpty()) {
737 logger
.debug("Got new recipient, number is unknown");
738 return addNewRecipient(connection
, new RecipientAddress(null, number
));
741 return recipient
.get().id();
744 private RecipientId
addNewRecipient(
745 final Connection connection
, final RecipientAddress address
746 ) throws SQLException
{
749 INSERT INTO %s (number, uuid, pni)
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
);
764 throw new RuntimeException("Failed to add new recipient to database");
769 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
773 SET number = NULL, uuid = NULL, pni = NULL
776 ).formatted(TABLE_RECIPIENT
);
777 try (final var statement
= connection
.prepareStatement(sql
)) {
778 statement
.setLong(1, recipientId
.id());
779 statement
.executeUpdate();
783 private void updateRecipientAddress(
784 Connection connection
, RecipientId recipientId
, final RecipientAddress address
785 ) throws SQLException
{
789 SET number = ?, uuid = ?, pni = ?, username = ?
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();
803 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
809 ).formatted(TABLE_RECIPIENT
);
810 try (final var statement
= connection
.prepareStatement(sql
)) {
811 statement
.setLong(1, recipientId
.id());
812 statement
.executeUpdate();
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
);
825 final var profileKey
= getProfileKey(connection
, recipientId
);
826 if (profileKey
== null) {
827 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
828 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
831 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
832 if (profileKeyCredential
== null) {
833 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
834 toBeMergedRecipientId
);
835 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
838 final var profile
= getProfile(connection
, recipientId
);
839 if (profile
== null) {
840 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
841 storeProfile(connection
, recipientId
, toBeMergedProfile
);
844 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
847 private Optional
<RecipientWithAddress
> findByNumber(
848 final Connection connection
, final String number
849 ) throws SQLException
{
851 SELECT r._id, r.number, r.uuid, r.pni, r.username
855 """.formatted(TABLE_RECIPIENT
);
856 try (final var statement
= connection
.prepareStatement(sql
)) {
857 statement
.setString(1, number
);
858 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
862 private Optional
<RecipientWithAddress
> findByUsername(
863 final Connection connection
, final String username
864 ) throws SQLException
{
866 SELECT r._id, r.number, r.uuid, r.pni, r.username
870 """.formatted(TABLE_RECIPIENT
);
871 try (final var statement
= connection
.prepareStatement(sql
)) {
872 statement
.setString(1, username
);
873 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
877 private Optional
<RecipientWithAddress
> findByServiceId(
878 final Connection connection
, final ServiceId serviceId
879 ) throws SQLException
{
881 SELECT r._id, r.number, r.uuid, r.pni, r.username
883 WHERE r.uuid = ? OR r.pni = ?
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
);
892 private Set
<RecipientWithAddress
> findAllByAddress(
893 final Connection connection
, final RecipientAddress address
894 ) throws SQLException
{
896 SELECT r._id, r.number, r.uuid, r.pni, r.username
898 WHERE r.uuid = ?1 OR r.pni = ?1 OR
899 r.uuid = ?2 OR r.pni = ?2 OR
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());
913 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
916 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
918 WHERE r._id = ? AND (%s)
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);
927 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
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);
941 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
942 final Connection connection
, final RecipientId recipientId
943 ) throws SQLException
{
946 SELECT r.profile_key_credential
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
)
958 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
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
963 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
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);
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
);
980 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
981 return new RecipientId(resultSet
.getLong("_id"), this);
984 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
985 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
986 getRecipientAddressFromResultSet(resultSet
));
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
));
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"));
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
1023 : Arrays
.stream(profileCapabilities
.split(","))
1024 .map(Profile
.Capability
::valueOfOrNull
)
1025 .filter(Objects
::nonNull
)
1026 .collect(Collectors
.toSet()));
1029 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1030 final var profileKey
= resultSet
.getBytes("profile_key");
1032 if (profileKey
== null) {
1036 return new ProfileKey(profileKey
);
1037 } catch (InvalidInputException ignored
) {
1042 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1043 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1045 if (profileKeyCredential
== null) {
1049 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1050 } catch (Throwable ignored
) {
1055 public interface RecipientMergeHandler
{
1057 void mergeRecipients(
1058 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1059 ) throws SQLException
;
1062 private class HelperStore
implements MergeRecipientHelper
.Store
{
1064 private final Connection connection
;
1066 public HelperStore(final Connection connection
) {
1067 this.connection
= connection
;
1071 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1072 return RecipientStore
.this.findAllByAddress(connection
, address
);
1076 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1077 return RecipientStore
.this.addNewRecipient(connection
, address
);
1081 public void updateRecipientAddress(
1082 final RecipientId recipientId
, final RecipientAddress address
1083 ) throws SQLException
{
1084 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1088 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1089 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);