1 package org
.asamk
.signal
.manager
.storage
.recipients
;
3 import org
.asamk
.signal
.manager
.api
.Contact
;
4 import org
.asamk
.signal
.manager
.api
.Pair
;
5 import org
.asamk
.signal
.manager
.api
.Profile
;
6 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
7 import org
.asamk
.signal
.manager
.storage
.Database
;
8 import org
.asamk
.signal
.manager
.storage
.Utils
;
9 import org
.asamk
.signal
.manager
.storage
.contacts
.ContactsStore
;
10 import org
.asamk
.signal
.manager
.storage
.profiles
.ProfileStore
;
11 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
12 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
13 import org
.signal
.libsignal
.zkgroup
.profiles
.ExpiringProfileKeyCredential
;
14 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
15 import org
.slf4j
.Logger
;
16 import org
.slf4j
.LoggerFactory
;
17 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
18 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
19 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
20 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
21 import org
.whispersystems
.signalservice
.api
.storage
.StorageId
;
23 import java
.sql
.Connection
;
24 import java
.sql
.ResultSet
;
25 import java
.sql
.SQLException
;
26 import java
.sql
.Types
;
27 import java
.util
.ArrayList
;
28 import java
.util
.Arrays
;
29 import java
.util
.Collection
;
30 import java
.util
.HashMap
;
31 import java
.util
.List
;
33 import java
.util
.Objects
;
34 import java
.util
.Optional
;
36 import java
.util
.function
.Supplier
;
37 import java
.util
.stream
.Collectors
;
39 public class RecipientStore
implements RecipientIdCreator
, RecipientResolver
, RecipientTrustedResolver
, ContactsStore
, ProfileStore
{
41 private static final Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
42 private static final String TABLE_RECIPIENT
= "recipient";
43 private static final String SQL_IS_CONTACT
= "r.given_name IS NOT NULL OR r.family_name IS NOT NULL OR r.nick_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";
45 private final RecipientMergeHandler recipientMergeHandler
;
46 private final SelfAddressProvider selfAddressProvider
;
47 private final SelfProfileKeyProvider selfProfileKeyProvider
;
48 private final Database database
;
50 private final Object recipientsLock
= new Object();
51 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
53 private final Map
<ServiceId
, RecipientWithAddress
> recipientAddressCache
= new HashMap
<>();
55 public static void createSql(Connection connection
) throws SQLException
{
56 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
57 try (final var statement
= connection
.createStatement()) {
58 statement
.executeUpdate("""
59 CREATE TABLE recipient (
60 _id INTEGER PRIMARY KEY AUTOINCREMENT,
61 storage_id BLOB UNIQUE,
67 unregistered_timestamp INTEGER,
69 profile_key_credential BLOB,
76 expiration_time INTEGER NOT NULL DEFAULT 0,
77 mute_until INTEGER NOT NULL DEFAULT 0,
78 blocked INTEGER NOT NULL DEFAULT FALSE,
79 archived INTEGER NOT NULL DEFAULT FALSE,
80 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
81 hide_story INTEGER NOT NULL DEFAULT FALSE,
82 hidden INTEGER NOT NULL DEFAULT FALSE,
84 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
85 profile_given_name TEXT,
86 profile_family_name TEXT,
88 profile_about_emoji TEXT,
89 profile_avatar_url_path TEXT,
90 profile_mobile_coin_address BLOB,
91 profile_unidentified_access_mode TEXT,
92 profile_capabilities TEXT
98 public RecipientStore(
99 final RecipientMergeHandler recipientMergeHandler
,
100 final SelfAddressProvider selfAddressProvider
,
101 final SelfProfileKeyProvider selfProfileKeyProvider
,
102 final Database database
104 this.recipientMergeHandler
= recipientMergeHandler
;
105 this.selfAddressProvider
= selfAddressProvider
;
106 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
107 this.database
= database
;
110 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
111 try (final var connection
= database
.getConnection()) {
112 return resolveRecipientAddress(connection
, recipientId
);
113 } catch (SQLException e
) {
114 throw new RuntimeException("Failed read from recipient store", e
);
118 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
123 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
125 ).formatted(TABLE_RECIPIENT
);
126 try (final var connection
= database
.getConnection()) {
127 try (final var statement
= connection
.prepareStatement(sql
)) {
128 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
129 return result
.toList();
132 } catch (SQLException e
) {
133 throw new RuntimeException("Failed read from recipient store", e
);
138 public RecipientId
resolveRecipient(final long rawRecipientId
) {
145 ).formatted(TABLE_RECIPIENT
);
146 try (final var connection
= database
.getConnection()) {
147 try (final var statement
= connection
.prepareStatement(sql
)) {
148 statement
.setLong(1, rawRecipientId
);
149 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
151 } catch (SQLException e
) {
152 throw new RuntimeException("Failed read from recipient store", e
);
157 public RecipientId
resolveRecipient(final String identifier
) {
158 final var serviceId
= ServiceId
.parseOrNull(identifier
);
159 if (serviceId
!= null) {
160 return resolveRecipient(serviceId
);
162 return resolveRecipientByNumber(identifier
);
166 private RecipientId
resolveRecipientByNumber(final String number
) {
167 synchronized (recipientsLock
) {
168 final RecipientId recipientId
;
169 try (final var connection
= database
.getConnection()) {
170 connection
.setAutoCommit(false);
171 recipientId
= resolveRecipientLocked(connection
, number
);
173 } catch (SQLException e
) {
174 throw new RuntimeException("Failed read recipient store", e
);
181 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
182 synchronized (recipientsLock
) {
183 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
184 if (recipientWithAddress
!= null) {
185 return recipientWithAddress
.id();
187 try (final var connection
= database
.getConnection()) {
188 connection
.setAutoCommit(false);
189 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
192 } catch (SQLException e
) {
193 throw new RuntimeException("Failed read recipient store", e
);
199 * Should only be used for recipientIds from the database.
200 * Where the foreign key relations ensure a valid recipientId.
203 public RecipientId
create(final long recipientId
) {
204 return new RecipientId(recipientId
, this);
207 public RecipientId
resolveRecipientByNumber(
208 final String number
, Supplier
<ServiceId
> serviceIdSupplier
209 ) throws UnregisteredRecipientException
{
210 final Optional
<RecipientWithAddress
> byNumber
;
211 try (final var connection
= database
.getConnection()) {
212 byNumber
= findByNumber(connection
, number
);
213 } catch (SQLException e
) {
214 throw new RuntimeException("Failed read from recipient store", e
);
216 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
217 final var serviceId
= serviceIdSupplier
.get();
218 if (serviceId
== null) {
219 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
223 return resolveRecipient(serviceId
);
225 return byNumber
.get().id();
228 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
229 final Optional
<RecipientWithAddress
> byNumber
;
230 try (final var connection
= database
.getConnection()) {
231 byNumber
= findByNumber(connection
, number
);
232 } catch (SQLException e
) {
233 throw new RuntimeException("Failed read from recipient store", e
);
235 return byNumber
.map(RecipientWithAddress
::id
);
238 public RecipientId
resolveRecipientByUsername(
239 final String username
, Supplier
<ACI
> aciSupplier
240 ) throws UnregisteredRecipientException
{
241 final Optional
<RecipientWithAddress
> byUsername
;
242 try (final var connection
= database
.getConnection()) {
243 byUsername
= findByUsername(connection
, username
);
244 } catch (SQLException e
) {
245 throw new RuntimeException("Failed read from recipient store", e
);
247 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
248 final var aci
= aciSupplier
.get();
250 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
255 return resolveRecipientTrusted(aci
, username
);
257 return byUsername
.get().id();
260 public RecipientId
resolveRecipient(RecipientAddress address
) {
261 synchronized (recipientsLock
) {
262 final RecipientId recipientId
;
263 try (final var connection
= database
.getConnection()) {
264 connection
.setAutoCommit(false);
265 recipientId
= resolveRecipientLocked(connection
, address
);
267 } catch (SQLException e
) {
268 throw new RuntimeException("Failed read recipient store", e
);
274 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
275 return resolveRecipientLocked(connection
, address
);
279 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
280 return resolveRecipientTrusted(address
, true);
284 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
285 return resolveRecipientTrusted(address
, false);
288 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
289 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
290 if (!pair
.second().isEmpty()) {
291 mergeRecipients(connection
, pair
.first(), pair
.second());
297 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
298 return resolveRecipientTrusted(new RecipientAddress(address
));
302 public RecipientId
resolveRecipientTrusted(
303 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
305 return resolveRecipientTrusted(new RecipientAddress(aci
, pni
, number
, Optional
.empty()));
309 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
310 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
314 public void storeContact(RecipientId recipientId
, final Contact contact
) {
315 try (final var connection
= database
.getConnection()) {
316 storeContact(connection
, recipientId
, contact
);
317 } catch (SQLException e
) {
318 throw new RuntimeException("Failed update recipient store", e
);
323 public Contact
getContact(RecipientId recipientId
) {
324 try (final var connection
= database
.getConnection()) {
325 return getContact(connection
, recipientId
);
326 } catch (SQLException e
) {
327 throw new RuntimeException("Failed read from recipient store", e
);
332 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
335 SELECT r._id, r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
337 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
339 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
340 try (final var connection
= database
.getConnection()) {
341 try (final var statement
= connection
.prepareStatement(sql
)) {
342 try (var result
= Utils
.executeQueryForStream(statement
,
343 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
344 getContactFromResultSet(resultSet
)))) {
345 return result
.toList();
348 } catch (SQLException e
) {
349 throw new RuntimeException("Failed read from recipient store", e
);
353 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
357 r.number, r.aci, r.pni, r.username,
358 r.profile_key, r.profile_key_credential,
359 r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
360 r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
365 ).formatted(TABLE_RECIPIENT
);
366 try (final var statement
= connection
.prepareStatement(sql
)) {
367 statement
.setLong(1, recipientId
.id());
368 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
372 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
376 r.number, r.aci, r.pni, r.username,
377 r.profile_key, r.profile_key_credential,
378 r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
379 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,
382 WHERE r.storage_id = ?
384 ).formatted(TABLE_RECIPIENT
);
385 try (final var statement
= connection
.prepareStatement(sql
)) {
386 statement
.setBytes(1, storageId
.getRaw());
387 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
391 public List
<Recipient
> getRecipients(
392 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
394 final var sqlWhere
= new ArrayList
<String
>();
396 sqlWhere
.add("r.unregistered_timestamp IS NULL");
397 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
398 sqlWhere
.add("r.hidden = FALSE");
400 if (blocked
.isPresent()) {
401 sqlWhere
.add("r.blocked = ?");
403 if (!recipientIds
.isEmpty()) {
404 final var recipientIdsCommaSeparated
= recipientIds
.stream()
405 .map(recipientId
-> String
.valueOf(recipientId
.id()))
406 .collect(Collectors
.joining(","));
407 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
412 r.number, r.aci, r.pni, r.username,
413 r.profile_key, r.profile_key_credential,
414 r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
415 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,
418 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
420 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
421 final var selfAddress
= selfAddressProvider
.getSelfAddress();
422 try (final var connection
= database
.getConnection()) {
423 try (final var statement
= connection
.prepareStatement(sql
)) {
424 if (blocked
.isPresent()) {
425 statement
.setBoolean(1, blocked
.get());
427 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
428 return result
.filter(r
-> name
.isEmpty() || (
429 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
430 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
431 if (r
.getAddress().matches(selfAddress
)) {
432 return Recipient
.newBuilder(r
)
433 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
440 } catch (SQLException e
) {
441 throw new RuntimeException("Failed read from recipient store", e
);
445 public Set
<String
> getAllNumbers() {
450 WHERE r.number IS NOT NULL
452 ).formatted(TABLE_RECIPIENT
);
453 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
454 try (final var connection
= database
.getConnection()) {
455 try (final var statement
= connection
.prepareStatement(sql
)) {
456 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
457 .filter(Objects
::nonNull
)
458 .filter(n
-> !n
.equals(selfNumber
))
463 } catch (NumberFormatException e
) {
467 .collect(Collectors
.toSet());
469 } catch (SQLException e
) {
470 throw new RuntimeException("Failed read from recipient store", e
);
474 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
477 SELECT r.aci, r.profile_key
479 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
481 ).formatted(TABLE_RECIPIENT
);
482 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
483 try (final var connection
= database
.getConnection()) {
484 try (final var statement
= connection
.prepareStatement(sql
)) {
485 return Utils
.executeQueryForStream(statement
, resultSet
-> {
486 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
487 if (aci
.equals(selfAci
)) {
488 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
490 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
491 return new Pair
<>(aci
, profileKey
);
492 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
494 } catch (SQLException e
) {
495 throw new RuntimeException("Failed read from recipient store", e
);
499 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
504 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
506 ).formatted(TABLE_RECIPIENT
);
507 try (final var statement
= connection
.prepareStatement(sql
)) {
508 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
512 public void setMissingStorageIds() {
513 final var selectSql
= (
517 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
519 ).formatted(TABLE_RECIPIENT
);
520 final var updateSql
= (
526 ).formatted(TABLE_RECIPIENT
);
527 try (final var connection
= database
.getConnection()) {
528 connection
.setAutoCommit(false);
529 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
530 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
532 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
533 for (final var recipientId
: recipientIds
) {
534 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
535 updateStmt
.setLong(2, recipientId
.id());
536 updateStmt
.executeUpdate();
541 } catch (SQLException e
) {
542 throw new RuntimeException("Failed update recipient store", e
);
547 public void deleteContact(RecipientId recipientId
) {
548 storeContact(recipientId
, null);
551 public void deleteRecipientData(RecipientId recipientId
) {
552 logger
.debug("Deleting recipient data for {}", recipientId
);
553 synchronized (recipientsLock
) {
554 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
555 try (final var connection
= database
.getConnection()) {
556 connection
.setAutoCommit(false);
557 storeContact(connection
, recipientId
, null);
558 storeProfile(connection
, recipientId
, null);
559 storeProfileKey(connection
, recipientId
, null, false);
560 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
561 deleteRecipient(connection
, recipientId
);
563 } catch (SQLException e
) {
564 throw new RuntimeException("Failed update recipient store", e
);
570 public Profile
getProfile(final RecipientId recipientId
) {
571 try (final var connection
= database
.getConnection()) {
572 return getProfile(connection
, recipientId
);
573 } catch (SQLException e
) {
574 throw new RuntimeException("Failed read from recipient store", e
);
579 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
580 try (final var connection
= database
.getConnection()) {
581 return getProfileKey(connection
, recipientId
);
582 } catch (SQLException e
) {
583 throw new RuntimeException("Failed read from recipient store", e
);
588 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
589 try (final var connection
= database
.getConnection()) {
590 return getExpiringProfileKeyCredential(connection
, recipientId
);
591 } catch (SQLException e
) {
592 throw new RuntimeException("Failed read from recipient store", e
);
597 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
598 try (final var connection
= database
.getConnection()) {
599 storeProfile(connection
, recipientId
, profile
);
600 } catch (SQLException e
) {
601 throw new RuntimeException("Failed update recipient store", e
);
606 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
607 try (final var connection
= database
.getConnection()) {
608 storeProfileKey(connection
, recipientId
, profileKey
);
609 } catch (SQLException e
) {
610 throw new RuntimeException("Failed update recipient store", e
);
614 public void storeProfileKey(
615 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
616 ) throws SQLException
{
617 storeProfileKey(connection
, recipientId
, profileKey
, true);
621 public void storeExpiringProfileKeyCredential(
622 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
624 try (final var connection
= database
.getConnection()) {
625 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
626 } catch (SQLException e
) {
627 throw new RuntimeException("Failed update recipient store", e
);
631 public void rotateSelfStorageId() {
632 try (final var connection
= database
.getConnection()) {
633 rotateSelfStorageId(connection
);
634 } catch (SQLException e
) {
635 throw new RuntimeException("Failed update recipient store", e
);
639 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
640 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
641 rotateStorageId(connection
, selfRecipientId
);
644 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
645 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
646 return rotateStorageId(connection
, selfRecipientId
);
649 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
652 FROM %s r WHERE r.storage_id IS NOT NULL AND r._id != ? AND (r.aci IS NOT NULL OR r.pni IS NOT NULL)
653 """.formatted(TABLE_RECIPIENT
);
654 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
655 try (final var statement
= connection
.prepareStatement(sql
)) {
656 statement
.setLong(1, selfRecipientId
.id());
657 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
661 public void updateStorageId(
662 Connection connection
, RecipientId recipientId
, StorageId storageId
663 ) throws SQLException
{
670 ).formatted(TABLE_RECIPIENT
);
671 try (final var statement
= connection
.prepareStatement(sql
)) {
672 statement
.setBytes(1, storageId
.getRaw());
673 statement
.setLong(2, recipientId
.id());
674 statement
.executeUpdate();
678 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
685 ).formatted(TABLE_RECIPIENT
);
686 try (final var statement
= connection
.prepareStatement(sql
)) {
687 for (final var entry
: storageIdMap
.entrySet()) {
688 statement
.setBytes(1, entry
.getValue().getRaw());
689 statement
.setLong(2, entry
.getKey().id());
690 statement
.executeUpdate();
695 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
696 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
697 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
700 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
703 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
704 """.formatted(TABLE_RECIPIENT
);
705 try (final var statement
= connection
.prepareStatement(sql
)) {
706 statement
.setLong(1, recipientId
.id());
707 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
708 if (storageId
.isPresent()) {
709 return storageId
.get();
712 return rotateStorageId(connection
, recipientId
);
715 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
716 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
717 updateStorageId(connection
, recipientId
, newStorageId
);
721 public void storeStorageRecord(
722 final Connection connection
,
723 final RecipientId recipientId
,
724 final StorageId storageId
,
725 final byte[] storageRecord
726 ) throws SQLException
{
727 final var deleteSql
= (
730 SET storage_id = NULL
733 ).formatted(TABLE_RECIPIENT
);
734 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
735 statement
.setBytes(1, storageId
.getRaw());
736 statement
.executeUpdate();
738 final var insertSql
= (
741 SET storage_id = ?, storage_record = ?
744 ).formatted(TABLE_RECIPIENT
);
745 try (final var statement
= connection
.prepareStatement(insertSql
)) {
746 statement
.setBytes(1, storageId
.getRaw());
747 if (storageRecord
== null) {
748 statement
.setNull(2, Types
.BLOB
);
750 statement
.setBytes(2, storageRecord
);
752 statement
.setLong(3, recipientId
.id());
753 statement
.executeUpdate();
757 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
758 logger
.debug("Migrating legacy recipients to database");
759 long start
= System
.nanoTime();
762 INSERT INTO %s (_id, number, aci)
765 ).formatted(TABLE_RECIPIENT
);
766 try (final var connection
= database
.getConnection()) {
767 connection
.setAutoCommit(false);
768 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
769 statement
.executeUpdate();
771 try (final var statement
= connection
.prepareStatement(sql
)) {
772 for (final var recipient
: recipients
.values()) {
773 statement
.setLong(1, recipient
.getRecipientId().id());
774 statement
.setString(2, recipient
.getAddress().number().orElse(null));
775 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
776 statement
.executeUpdate();
779 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
781 for (final var recipient
: recipients
.values()) {
782 if (recipient
.getContact() != null) {
783 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
785 if (recipient
.getProfile() != null) {
786 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
788 if (recipient
.getProfileKey() != null) {
789 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
791 if (recipient
.getExpiringProfileKeyCredential() != null) {
792 storeExpiringProfileKeyCredential(connection
,
793 recipient
.getRecipientId(),
794 recipient
.getExpiringProfileKeyCredential());
798 } catch (SQLException e
) {
799 throw new RuntimeException("Failed update recipient store", e
);
801 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
804 long getActualRecipientId(long recipientId
) {
805 while (recipientsMerged
.containsKey(recipientId
)) {
806 final var newRecipientId
= recipientsMerged
.get(recipientId
);
807 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
808 recipientId
= newRecipientId
;
813 public void storeContact(
814 final Connection connection
, final RecipientId recipientId
, final Contact contact
815 ) throws SQLException
{
819 SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?
822 ).formatted(TABLE_RECIPIENT
);
823 try (final var statement
= connection
.prepareStatement(sql
)) {
824 statement
.setString(1, contact
== null ?
null : contact
.givenName());
825 statement
.setString(2, contact
== null ?
null : contact
.familyName());
826 statement
.setString(3, contact
== null ?
null : contact
.nickName());
827 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
828 statement
.setLong(5, contact
== null ?
0 : contact
.muteUntil());
829 statement
.setBoolean(6, contact
!= null && contact
.hideStory());
830 statement
.setBoolean(7, contact
!= null && contact
.isProfileSharingEnabled());
831 statement
.setString(8, contact
== null ?
null : contact
.color());
832 statement
.setBoolean(9, contact
!= null && contact
.isBlocked());
833 statement
.setBoolean(10, contact
!= null && contact
.isArchived());
834 if (contact
== null || contact
.unregisteredTimestamp() == null) {
835 statement
.setNull(11, Types
.INTEGER
);
837 statement
.setLong(11, contact
.unregisteredTimestamp());
839 statement
.setLong(12, recipientId
.id());
840 statement
.executeUpdate();
842 if (contact
!= null && contact
.unregisteredTimestamp() != null) {
843 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
845 rotateStorageId(connection
, recipientId
);
848 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
849 final Connection connection
, final List
<StorageId
> storageIds
850 ) throws SQLException
{
854 SET storage_id = NULL
855 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
857 ).formatted(TABLE_RECIPIENT
);
859 try (final var statement
= connection
.prepareStatement(sql
)) {
860 for (final var storageId
: storageIds
) {
861 statement
.setBytes(1, storageId
.getRaw());
862 count
+= statement
.executeUpdate();
868 public void markUnregistered(final Set
<String
> unregisteredUsers
) {
869 logger
.debug("Marking {} numbers as unregistered", unregisteredUsers
.size());
870 try (final var connection
= database
.getConnection()) {
871 connection
.setAutoCommit(false);
872 for (final var number
: unregisteredUsers
) {
873 final var recipient
= findByNumber(connection
, number
);
874 if (recipient
.isPresent()) {
875 final var recipientId
= recipient
.get().id();
876 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
880 } catch (SQLException e
) {
881 throw new RuntimeException("Failed update recipient store", e
);
885 private void markUnregisteredAndSplitIfNecessary(
886 final Connection connection
, final RecipientId recipientId
887 ) throws SQLException
{
888 markUnregistered(connection
, recipientId
);
889 final var address
= resolveRecipientAddress(connection
, recipientId
);
890 if (address
.aci().isPresent() && address
.pni().isPresent()) {
891 final var numberAddress
= new RecipientAddress(address
.pni().get(), address
.number().orElse(null));
892 updateRecipientAddress(connection
, recipientId
, address
.removeIdentifiersFrom(numberAddress
));
893 addNewRecipient(connection
, numberAddress
);
897 private void markRegistered(
898 final Connection connection
, final RecipientId recipientId
899 ) throws SQLException
{
903 SET unregistered_timestamp = ?
906 ).formatted(TABLE_RECIPIENT
);
907 try (final var statement
= connection
.prepareStatement(sql
)) {
908 statement
.setNull(1, Types
.INTEGER
);
909 statement
.setLong(2, recipientId
.id());
910 statement
.executeUpdate();
914 private void markUnregistered(
915 final Connection connection
, final RecipientId recipientId
916 ) throws SQLException
{
920 SET unregistered_timestamp = ?
921 WHERE _id = ? AND unregistered_timestamp IS NULL
923 ).formatted(TABLE_RECIPIENT
);
924 try (final var statement
= connection
.prepareStatement(sql
)) {
925 statement
.setLong(1, System
.currentTimeMillis());
926 statement
.setLong(2, recipientId
.id());
927 statement
.executeUpdate();
931 private void storeExpiringProfileKeyCredential(
932 final Connection connection
,
933 final RecipientId recipientId
,
934 final ExpiringProfileKeyCredential profileKeyCredential
935 ) throws SQLException
{
939 SET profile_key_credential = ?
942 ).formatted(TABLE_RECIPIENT
);
943 try (final var statement
= connection
.prepareStatement(sql
)) {
944 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
945 statement
.setLong(2, recipientId
.id());
946 statement
.executeUpdate();
950 public void storeProfile(
951 final Connection connection
, final RecipientId recipientId
, final Profile profile
952 ) throws SQLException
{
956 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 = ?
959 ).formatted(TABLE_RECIPIENT
);
960 try (final var statement
= connection
.prepareStatement(sql
)) {
961 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
962 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
963 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
964 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
965 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
966 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
967 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
968 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
969 statement
.setString(9,
972 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
973 statement
.setLong(10, recipientId
.id());
974 statement
.executeUpdate();
976 rotateStorageId(connection
, recipientId
);
979 private void storeProfileKey(
980 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
981 ) throws SQLException
{
982 if (profileKey
!= null) {
983 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
984 if (profileKey
.equals(recipientProfileKey
)) {
985 final var recipientProfile
= getProfile(connection
, recipientId
);
986 if (recipientProfile
== null || (
987 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
988 && recipientProfile
.getUnidentifiedAccessMode()
989 != Profile
.UnidentifiedAccessMode
.DISABLED
999 SET profile_key = ?, profile_key_credential = NULL%s
1002 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
1003 try (final var statement
= connection
.prepareStatement(sql
)) {
1004 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
1005 statement
.setLong(2, recipientId
.id());
1006 statement
.executeUpdate();
1008 rotateStorageId(connection
, recipientId
);
1011 private RecipientAddress
resolveRecipientAddress(
1012 final Connection connection
, final RecipientId recipientId
1013 ) throws SQLException
{
1016 SELECT r.number, r.aci, r.pni, r.username
1020 ).formatted(TABLE_RECIPIENT
);
1021 try (final var statement
= connection
.prepareStatement(sql
)) {
1022 statement
.setLong(1, recipientId
.id());
1023 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
1027 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1028 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1029 synchronized (recipientsLock
) {
1030 try (final var connection
= database
.getConnection()) {
1031 connection
.setAutoCommit(false);
1032 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1033 connection
.commit();
1034 } catch (SQLException e
) {
1035 throw new RuntimeException("Failed update recipient store", e
);
1039 if (!pair
.second().isEmpty()) {
1040 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1041 try (final var connection
= database
.getConnection()) {
1042 connection
.setAutoCommit(false);
1043 mergeRecipients(connection
, pair
.first(), pair
.second());
1044 connection
.commit();
1045 } catch (SQLException e
) {
1046 throw new RuntimeException("Failed update recipient store", e
);
1049 return pair
.first();
1052 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1053 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1054 ) throws SQLException
{
1055 if (address
.hasSingleIdentifier() || (
1056 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1058 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1060 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1061 markRegistered(connection
, pair
.first());
1063 for (final var toBeMergedRecipientId
: pair
.second()) {
1064 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1070 private void mergeRecipients(
1071 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1072 ) throws SQLException
{
1073 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1074 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1075 deleteRecipient(connection
, toBeMergedRecipientId
);
1076 synchronized (recipientsLock
) {
1077 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1082 private RecipientId
resolveRecipientLocked(
1083 Connection connection
, RecipientAddress address
1084 ) throws SQLException
{
1085 final var byAci
= address
.aci().isEmpty()
1086 ? Optional
.<RecipientWithAddress
>empty()
1087 : findByServiceId(connection
, address
.aci().get());
1089 if (byAci
.isPresent()) {
1090 return byAci
.get().id();
1093 final var byPni
= address
.pni().isEmpty()
1094 ? Optional
.<RecipientWithAddress
>empty()
1095 : findByServiceId(connection
, address
.pni().get());
1097 if (byPni
.isPresent()) {
1098 return byPni
.get().id();
1101 final var byNumber
= address
.number().isEmpty()
1102 ? Optional
.<RecipientWithAddress
>empty()
1103 : findByNumber(connection
, address
.number().get());
1105 if (byNumber
.isPresent()) {
1106 return byNumber
.get().id();
1109 logger
.debug("Got new recipient, both serviceId and number are unknown");
1111 if (address
.serviceId().isEmpty()) {
1112 return addNewRecipient(connection
, address
);
1115 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1118 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1119 final var recipient
= findByServiceId(connection
, serviceId
);
1121 if (recipient
.isEmpty()) {
1122 logger
.debug("Got new recipient, serviceId is unknown");
1123 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1126 return recipient
.get().id();
1129 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1130 final var recipient
= findByNumber(connection
, number
);
1132 if (recipient
.isEmpty()) {
1133 logger
.debug("Got new recipient, number is unknown");
1134 return addNewRecipient(connection
, new RecipientAddress(number
));
1137 return recipient
.get().id();
1140 private RecipientId
addNewRecipient(
1141 final Connection connection
, final RecipientAddress address
1142 ) throws SQLException
{
1145 INSERT INTO %s (number, aci, pni, username)
1149 ).formatted(TABLE_RECIPIENT
);
1150 try (final var statement
= connection
.prepareStatement(sql
)) {
1151 statement
.setString(1, address
.number().orElse(null));
1152 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1153 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1154 statement
.setString(4, address
.username().orElse(null));
1155 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1156 if (generatedKey
.isPresent()) {
1157 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1158 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1161 throw new RuntimeException("Failed to add new recipient to database");
1166 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1167 synchronized (recipientsLock
) {
1168 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1172 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1175 ).formatted(TABLE_RECIPIENT
);
1176 try (final var statement
= connection
.prepareStatement(sql
)) {
1177 statement
.setLong(1, recipientId
.id());
1178 statement
.executeUpdate();
1183 private void updateRecipientAddress(
1184 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1185 ) throws SQLException
{
1186 synchronized (recipientsLock
) {
1187 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1191 SET number = ?, aci = ?, pni = ?, username = ?
1194 ).formatted(TABLE_RECIPIENT
);
1195 try (final var statement
= connection
.prepareStatement(sql
)) {
1196 statement
.setString(1, address
.number().orElse(null));
1197 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1198 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1199 statement
.setString(4, address
.username().orElse(null));
1200 statement
.setLong(5, recipientId
.id());
1201 statement
.executeUpdate();
1203 rotateStorageId(connection
, recipientId
);
1207 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1213 ).formatted(TABLE_RECIPIENT
);
1214 try (final var statement
= connection
.prepareStatement(sql
)) {
1215 statement
.setLong(1, recipientId
.id());
1216 statement
.executeUpdate();
1220 private void mergeRecipientsLocked(
1221 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1222 ) throws SQLException
{
1223 final var contact
= getContact(connection
, recipientId
);
1224 if (contact
== null) {
1225 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1226 storeContact(connection
, recipientId
, toBeMergedContact
);
1229 final var profileKey
= getProfileKey(connection
, recipientId
);
1230 if (profileKey
== null) {
1231 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1232 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1235 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1236 if (profileKeyCredential
== null) {
1237 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1238 toBeMergedRecipientId
);
1239 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1242 final var profile
= getProfile(connection
, recipientId
);
1243 if (profile
== null) {
1244 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1245 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1248 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1251 private Optional
<RecipientWithAddress
> findByNumber(
1252 final Connection connection
, final String number
1253 ) throws SQLException
{
1255 SELECT r._id, r.number, r.aci, r.pni, r.username
1259 """.formatted(TABLE_RECIPIENT
);
1260 try (final var statement
= connection
.prepareStatement(sql
)) {
1261 statement
.setString(1, number
);
1262 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1266 private Optional
<RecipientWithAddress
> findByUsername(
1267 final Connection connection
, final String username
1268 ) throws SQLException
{
1270 SELECT r._id, r.number, r.aci, r.pni, r.username
1272 WHERE r.username = ?
1274 """.formatted(TABLE_RECIPIENT
);
1275 try (final var statement
= connection
.prepareStatement(sql
)) {
1276 statement
.setString(1, username
);
1277 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1281 private Optional
<RecipientWithAddress
> findByServiceId(
1282 final Connection connection
, final ServiceId serviceId
1283 ) throws SQLException
{
1284 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1285 if (recipientWithAddress
.isPresent()) {
1286 return recipientWithAddress
;
1289 SELECT r._id, r.number, r.aci, r.pni, r.username
1293 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1294 try (final var statement
= connection
.prepareStatement(sql
)) {
1295 statement
.setString(1, serviceId
.toString());
1296 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1297 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1298 return recipientWithAddress
;
1302 private Set
<RecipientWithAddress
> findAllByAddress(
1303 final Connection connection
, final RecipientAddress address
1304 ) throws SQLException
{
1306 SELECT r._id, r.number, r.aci, r.pni, r.username
1312 """.formatted(TABLE_RECIPIENT
);
1313 try (final var statement
= connection
.prepareStatement(sql
)) {
1314 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1315 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1316 statement
.setString(3, address
.number().orElse(null));
1317 statement
.setString(4, address
.username().orElse(null));
1318 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1319 .collect(Collectors
.toSet());
1323 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1326 SELECT r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
1328 WHERE r._id = ? AND (%s)
1330 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1331 try (final var statement
= connection
.prepareStatement(sql
)) {
1332 statement
.setLong(1, recipientId
.id());
1333 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1337 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1338 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1339 if (recipientId
.equals(selfRecipientId
)) {
1340 return selfProfileKeyProvider
.getSelfProfileKey();
1344 SELECT r.profile_key
1348 ).formatted(TABLE_RECIPIENT
);
1349 try (final var statement
= connection
.prepareStatement(sql
)) {
1350 statement
.setLong(1, recipientId
.id());
1351 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1355 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1356 final Connection connection
, final RecipientId recipientId
1357 ) throws SQLException
{
1360 SELECT r.profile_key_credential
1364 ).formatted(TABLE_RECIPIENT
);
1365 try (final var statement
= connection
.prepareStatement(sql
)) {
1366 statement
.setLong(1, recipientId
.id());
1367 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1372 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1375 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
1377 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1379 ).formatted(TABLE_RECIPIENT
);
1380 try (final var statement
= connection
.prepareStatement(sql
)) {
1381 statement
.setLong(1, recipientId
.id());
1382 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1386 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1387 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrThrow
);
1388 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrThrow
);
1389 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1390 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1391 return new RecipientAddress(aci
, pni
, number
, username
);
1394 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1395 return new RecipientId(resultSet
.getLong("_id"), this);
1398 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1399 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1400 getRecipientAddressFromResultSet(resultSet
));
1403 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1404 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1405 getRecipientAddressFromResultSet(resultSet
),
1406 getContactFromResultSet(resultSet
),
1407 getProfileKeyFromResultSet(resultSet
),
1408 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1409 getProfileFromResultSet(resultSet
),
1410 getStorageRecordFromResultSet(resultSet
));
1413 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1414 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1415 return new Contact(resultSet
.getString("given_name"),
1416 resultSet
.getString("family_name"),
1417 resultSet
.getString("nick_name"),
1418 resultSet
.getString("color"),
1419 resultSet
.getInt("expiration_time"),
1420 resultSet
.getLong("mute_until"),
1421 resultSet
.getBoolean("hide_story"),
1422 resultSet
.getBoolean("blocked"),
1423 resultSet
.getBoolean("archived"),
1424 resultSet
.getBoolean("profile_sharing"),
1425 resultSet
.getBoolean("hidden"),
1426 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1429 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1430 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1431 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1432 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1433 resultSet
.getString("profile_given_name"),
1434 resultSet
.getString("profile_family_name"),
1435 resultSet
.getString("profile_about"),
1436 resultSet
.getString("profile_about_emoji"),
1437 resultSet
.getString("profile_avatar_url_path"),
1438 resultSet
.getBytes("profile_mobile_coin_address"),
1439 profileUnidentifiedAccessMode
== null
1440 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1441 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1442 profileCapabilities
== null
1444 : Arrays
.stream(profileCapabilities
.split(","))
1445 .map(Profile
.Capability
::valueOfOrNull
)
1446 .filter(Objects
::nonNull
)
1447 .collect(Collectors
.toSet()));
1450 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1451 final var profileKey
= resultSet
.getBytes("profile_key");
1453 if (profileKey
== null) {
1457 return new ProfileKey(profileKey
);
1458 } catch (InvalidInputException ignored
) {
1463 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1464 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1466 if (profileKeyCredential
== null) {
1470 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1471 } catch (Throwable ignored
) {
1476 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1477 final var storageId
= resultSet
.getBytes("storage_id");
1478 return StorageId
.forContact(storageId
);
1481 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1482 return resultSet
.getBytes("storage_record");
1485 public interface RecipientMergeHandler
{
1487 void mergeRecipients(
1488 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1489 ) throws SQLException
;
1492 private class HelperStore
implements MergeRecipientHelper
.Store
{
1494 private final Connection connection
;
1496 public HelperStore(final Connection connection
) {
1497 this.connection
= connection
;
1501 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1502 return RecipientStore
.this.findAllByAddress(connection
, address
);
1506 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1507 return RecipientStore
.this.addNewRecipient(connection
, address
);
1511 public void updateRecipientAddress(
1512 final RecipientId recipientId
, final RecipientAddress address
1513 ) throws SQLException
{
1514 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1518 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1519 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);