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 Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
52 private final Map
<ServiceId
, RecipientWithAddress
> recipientAddressCache
= new HashMap
<>();
54 public static void createSql(Connection connection
) throws SQLException
{
55 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
56 try (final var statement
= connection
.createStatement()) {
57 statement
.executeUpdate("""
58 CREATE TABLE recipient (
59 _id INTEGER PRIMARY KEY AUTOINCREMENT,
60 storage_id BLOB UNIQUE,
66 unregistered_timestamp INTEGER,
68 profile_key_credential BLOB,
69 needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
74 nick_name_given_name TEXT,
75 nick_name_family_name TEXT,
79 expiration_time INTEGER NOT NULL DEFAULT 0,
80 mute_until INTEGER NOT NULL DEFAULT 0,
81 blocked INTEGER NOT NULL DEFAULT FALSE,
82 archived INTEGER NOT NULL DEFAULT FALSE,
83 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
84 hide_story INTEGER NOT NULL DEFAULT FALSE,
85 hidden INTEGER NOT NULL DEFAULT FALSE,
87 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
88 profile_given_name TEXT,
89 profile_family_name TEXT,
91 profile_about_emoji TEXT,
92 profile_avatar_url_path TEXT,
93 profile_mobile_coin_address BLOB,
94 profile_unidentified_access_mode TEXT,
95 profile_capabilities TEXT
101 public RecipientStore(
102 final RecipientMergeHandler recipientMergeHandler
,
103 final SelfAddressProvider selfAddressProvider
,
104 final SelfProfileKeyProvider selfProfileKeyProvider
,
105 final Database database
107 this.recipientMergeHandler
= recipientMergeHandler
;
108 this.selfAddressProvider
= selfAddressProvider
;
109 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
110 this.database
= database
;
113 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
114 try (final var connection
= database
.getConnection()) {
115 return resolveRecipientAddress(connection
, recipientId
);
116 } catch (SQLException e
) {
117 throw new RuntimeException("Failed read from recipient store", e
);
121 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
126 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
128 ).formatted(TABLE_RECIPIENT
);
129 try (final var connection
= database
.getConnection()) {
130 try (final var statement
= connection
.prepareStatement(sql
)) {
131 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
132 return result
.toList();
135 } catch (SQLException e
) {
136 throw new RuntimeException("Failed read from recipient store", e
);
141 public RecipientId
resolveRecipient(final long rawRecipientId
) {
148 ).formatted(TABLE_RECIPIENT
);
149 try (final var connection
= database
.getConnection()) {
150 try (final var statement
= connection
.prepareStatement(sql
)) {
151 statement
.setLong(1, rawRecipientId
);
152 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
154 } catch (SQLException e
) {
155 throw new RuntimeException("Failed read from recipient store", e
);
160 public RecipientId
resolveRecipient(final String identifier
) {
161 final var serviceId
= ServiceId
.parseOrNull(identifier
);
162 if (serviceId
!= null) {
163 return resolveRecipient(serviceId
);
165 return resolveRecipientByNumber(identifier
);
169 private RecipientId
resolveRecipientByNumber(final String number
) {
170 final RecipientId recipientId
;
171 try (final var connection
= database
.getConnection()) {
172 connection
.setAutoCommit(false);
173 recipientId
= resolveRecipientLocked(connection
, number
);
175 } catch (SQLException e
) {
176 throw new RuntimeException("Failed read recipient store", e
);
182 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
183 try (final var connection
= database
.getConnection()) {
184 connection
.setAutoCommit(false);
185 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
186 if (recipientWithAddress
!= null) {
187 return recipientWithAddress
.id();
189 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
192 } catch (SQLException e
) {
193 throw new RuntimeException("Failed read recipient store", e
);
198 * Should only be used for recipientIds from the database.
199 * Where the foreign key relations ensure a valid recipientId.
202 public RecipientId
create(final long recipientId
) {
203 return new RecipientId(recipientId
, this);
206 public RecipientId
resolveRecipientByNumber(
207 final String number
, Supplier
<ServiceId
> serviceIdSupplier
208 ) throws UnregisteredRecipientException
{
209 final Optional
<RecipientWithAddress
> byNumber
;
210 try (final var connection
= database
.getConnection()) {
211 byNumber
= findByNumber(connection
, number
);
212 } catch (SQLException e
) {
213 throw new RuntimeException("Failed read from recipient store", e
);
215 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
216 final var serviceId
= serviceIdSupplier
.get();
217 if (serviceId
== null) {
218 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
222 return resolveRecipient(serviceId
);
224 return byNumber
.get().id();
227 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
228 final Optional
<RecipientWithAddress
> byNumber
;
229 try (final var connection
= database
.getConnection()) {
230 byNumber
= findByNumber(connection
, number
);
231 } catch (SQLException e
) {
232 throw new RuntimeException("Failed read from recipient store", e
);
234 return byNumber
.map(RecipientWithAddress
::id
);
237 public RecipientId
resolveRecipientByUsername(
238 final String username
, Supplier
<ACI
> aciSupplier
239 ) throws UnregisteredRecipientException
{
240 final Optional
<RecipientWithAddress
> byUsername
;
241 try (final var connection
= database
.getConnection()) {
242 byUsername
= findByUsername(connection
, username
);
243 } catch (SQLException e
) {
244 throw new RuntimeException("Failed read from recipient store", e
);
246 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
247 final var aci
= aciSupplier
.get();
249 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
254 return resolveRecipientTrusted(aci
, username
);
256 return byUsername
.get().id();
259 public RecipientId
resolveRecipient(RecipientAddress address
) {
260 final RecipientId recipientId
;
261 try (final var connection
= database
.getConnection()) {
262 connection
.setAutoCommit(false);
263 recipientId
= resolveRecipientLocked(connection
, address
);
265 } catch (SQLException e
) {
266 throw new RuntimeException("Failed read recipient store", e
);
271 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
272 return resolveRecipientLocked(connection
, address
);
276 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
277 return resolveRecipientTrusted(address
, true);
281 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
282 return resolveRecipientTrusted(address
, false);
285 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
286 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
287 if (!pair
.second().isEmpty()) {
288 mergeRecipients(connection
, pair
.first(), pair
.second());
294 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
295 return resolveRecipientTrusted(new RecipientAddress(address
));
299 public RecipientId
resolveRecipientTrusted(
300 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
302 return resolveRecipientTrusted(new RecipientAddress(aci
, pni
, number
, Optional
.empty()));
306 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
307 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
311 public void storeContact(RecipientId recipientId
, final Contact contact
) {
312 try (final var connection
= database
.getConnection()) {
313 storeContact(connection
, recipientId
, contact
);
314 } catch (SQLException e
) {
315 throw new RuntimeException("Failed update recipient store", e
);
320 public Contact
getContact(RecipientId recipientId
) {
321 try (final var connection
= database
.getConnection()) {
322 return getContact(connection
, recipientId
);
323 } catch (SQLException e
) {
324 throw new RuntimeException("Failed read from recipient store", e
);
329 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
332 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
334 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
336 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
337 try (final var connection
= database
.getConnection()) {
338 try (final var statement
= connection
.prepareStatement(sql
)) {
339 try (var result
= Utils
.executeQueryForStream(statement
,
340 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
341 getContactFromResultSet(resultSet
)))) {
342 return result
.toList();
345 } catch (SQLException e
) {
346 throw new RuntimeException("Failed read from recipient store", e
);
350 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
354 r.number, r.aci, r.pni, r.username,
355 r.profile_key, r.profile_key_credential,
356 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,
357 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,
362 ).formatted(TABLE_RECIPIENT
);
363 try (final var statement
= connection
.prepareStatement(sql
)) {
364 statement
.setLong(1, recipientId
.id());
365 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
369 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
373 r.number, r.aci, r.pni, r.username,
374 r.profile_key, r.profile_key_credential,
375 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,
376 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,
379 WHERE r.storage_id = ?
381 ).formatted(TABLE_RECIPIENT
);
382 try (final var statement
= connection
.prepareStatement(sql
)) {
383 statement
.setBytes(1, storageId
.getRaw());
384 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
388 public List
<Recipient
> getRecipients(
389 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
391 final var sqlWhere
= new ArrayList
<String
>();
393 sqlWhere
.add("r.unregistered_timestamp IS NULL");
394 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
395 sqlWhere
.add("r.hidden = FALSE");
397 if (blocked
.isPresent()) {
398 sqlWhere
.add("r.blocked = ?");
400 if (!recipientIds
.isEmpty()) {
401 final var recipientIdsCommaSeparated
= recipientIds
.stream()
402 .map(recipientId
-> String
.valueOf(recipientId
.id()))
403 .collect(Collectors
.joining(","));
404 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
409 r.number, r.aci, r.pni, r.username,
410 r.profile_key, r.profile_key_credential,
411 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,
412 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,
415 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
417 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
418 final var selfAddress
= selfAddressProvider
.getSelfAddress();
419 try (final var connection
= database
.getConnection()) {
420 try (final var statement
= connection
.prepareStatement(sql
)) {
421 if (blocked
.isPresent()) {
422 statement
.setBoolean(1, blocked
.get());
424 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
425 return result
.filter(r
-> name
.isEmpty() || (
426 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
427 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
428 if (r
.getAddress().matches(selfAddress
)) {
429 return Recipient
.newBuilder(r
)
430 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
437 } catch (SQLException e
) {
438 throw new RuntimeException("Failed read from recipient store", e
);
442 public Set
<String
> getAllNumbers() {
447 WHERE r.number IS NOT NULL
449 ).formatted(TABLE_RECIPIENT
);
450 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
451 try (final var connection
= database
.getConnection()) {
452 try (final var statement
= connection
.prepareStatement(sql
)) {
453 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
454 .filter(Objects
::nonNull
)
455 .filter(n
-> !n
.equals(selfNumber
))
460 } catch (NumberFormatException e
) {
464 .collect(Collectors
.toSet());
466 } catch (SQLException e
) {
467 throw new RuntimeException("Failed read from recipient store", e
);
471 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
474 SELECT r.aci, r.profile_key
476 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
478 ).formatted(TABLE_RECIPIENT
);
479 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
480 try (final var connection
= database
.getConnection()) {
481 try (final var statement
= connection
.prepareStatement(sql
)) {
482 return Utils
.executeQueryForStream(statement
, resultSet
-> {
483 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
484 if (aci
.equals(selfAci
)) {
485 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
487 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
488 return new Pair
<>(aci
, profileKey
);
489 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
491 } catch (SQLException e
) {
492 throw new RuntimeException("Failed read from recipient store", e
);
496 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
501 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
503 ).formatted(TABLE_RECIPIENT
);
504 try (final var statement
= connection
.prepareStatement(sql
)) {
505 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
509 public void setMissingStorageIds() {
510 final var selectSql
= (
514 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
516 ).formatted(TABLE_RECIPIENT
);
517 final var updateSql
= (
523 ).formatted(TABLE_RECIPIENT
);
524 try (final var connection
= database
.getConnection()) {
525 connection
.setAutoCommit(false);
526 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
527 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
529 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
530 for (final var recipientId
: recipientIds
) {
531 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
532 updateStmt
.setLong(2, recipientId
.id());
533 updateStmt
.executeUpdate();
538 } catch (SQLException e
) {
539 throw new RuntimeException("Failed update recipient store", e
);
544 public void deleteContact(RecipientId recipientId
) {
545 storeContact(recipientId
, null);
548 public void deleteRecipientData(RecipientId recipientId
) {
549 logger
.debug("Deleting recipient data for {}", recipientId
);
550 try (final var connection
= database
.getConnection()) {
551 connection
.setAutoCommit(false);
552 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
553 storeContact(connection
, recipientId
, null);
554 storeProfile(connection
, recipientId
, null);
555 storeProfileKey(connection
, recipientId
, null, false);
556 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
557 deleteRecipient(connection
, recipientId
);
559 } catch (SQLException e
) {
560 throw new RuntimeException("Failed update recipient store", e
);
565 public Profile
getProfile(final RecipientId recipientId
) {
566 try (final var connection
= database
.getConnection()) {
567 return getProfile(connection
, recipientId
);
568 } catch (SQLException e
) {
569 throw new RuntimeException("Failed read from recipient store", e
);
574 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
575 try (final var connection
= database
.getConnection()) {
576 return getProfileKey(connection
, recipientId
);
577 } catch (SQLException e
) {
578 throw new RuntimeException("Failed read from recipient store", e
);
583 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
584 try (final var connection
= database
.getConnection()) {
585 return getExpiringProfileKeyCredential(connection
, recipientId
);
586 } catch (SQLException e
) {
587 throw new RuntimeException("Failed read from recipient store", e
);
592 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
593 try (final var connection
= database
.getConnection()) {
594 storeProfile(connection
, recipientId
, profile
);
595 } catch (SQLException e
) {
596 throw new RuntimeException("Failed update recipient store", e
);
601 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
602 try (final var connection
= database
.getConnection()) {
603 storeProfileKey(connection
, recipientId
, profileKey
);
604 } catch (SQLException e
) {
605 throw new RuntimeException("Failed update recipient store", e
);
609 public void storeProfileKey(
610 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
611 ) throws SQLException
{
612 storeProfileKey(connection
, recipientId
, profileKey
, true);
616 public void storeExpiringProfileKeyCredential(
617 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
619 try (final var connection
= database
.getConnection()) {
620 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
621 } catch (SQLException e
) {
622 throw new RuntimeException("Failed update recipient store", e
);
626 public void rotateSelfStorageId() {
627 try (final var connection
= database
.getConnection()) {
628 rotateSelfStorageId(connection
);
629 } catch (SQLException e
) {
630 throw new RuntimeException("Failed update recipient store", e
);
634 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
635 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
636 rotateStorageId(connection
, selfRecipientId
);
639 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
640 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
641 return rotateStorageId(connection
, selfRecipientId
);
644 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
647 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)
648 """.formatted(TABLE_RECIPIENT
);
649 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
650 try (final var statement
= connection
.prepareStatement(sql
)) {
651 statement
.setLong(1, selfRecipientId
.id());
652 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
656 public void updateStorageId(
657 Connection connection
, RecipientId recipientId
, StorageId storageId
658 ) throws SQLException
{
665 ).formatted(TABLE_RECIPIENT
);
666 try (final var statement
= connection
.prepareStatement(sql
)) {
667 statement
.setBytes(1, storageId
.getRaw());
668 statement
.setLong(2, recipientId
.id());
669 statement
.executeUpdate();
673 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
680 ).formatted(TABLE_RECIPIENT
);
681 try (final var statement
= connection
.prepareStatement(sql
)) {
682 for (final var entry
: storageIdMap
.entrySet()) {
683 statement
.setBytes(1, entry
.getValue().getRaw());
684 statement
.setLong(2, entry
.getKey().id());
685 statement
.executeUpdate();
690 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
691 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
692 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
695 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
698 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
699 """.formatted(TABLE_RECIPIENT
);
700 try (final var statement
= connection
.prepareStatement(sql
)) {
701 statement
.setLong(1, recipientId
.id());
702 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
703 if (storageId
.isPresent()) {
704 return storageId
.get();
707 return rotateStorageId(connection
, recipientId
);
710 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
711 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
712 updateStorageId(connection
, recipientId
, newStorageId
);
716 public void storeStorageRecord(
717 final Connection connection
,
718 final RecipientId recipientId
,
719 final StorageId storageId
,
720 final byte[] storageRecord
721 ) throws SQLException
{
722 final var deleteSql
= (
725 SET storage_id = NULL
728 ).formatted(TABLE_RECIPIENT
);
729 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
730 statement
.setBytes(1, storageId
.getRaw());
731 statement
.executeUpdate();
733 final var insertSql
= (
736 SET storage_id = ?, storage_record = ?
739 ).formatted(TABLE_RECIPIENT
);
740 try (final var statement
= connection
.prepareStatement(insertSql
)) {
741 statement
.setBytes(1, storageId
.getRaw());
742 if (storageRecord
== null) {
743 statement
.setNull(2, Types
.BLOB
);
745 statement
.setBytes(2, storageRecord
);
747 statement
.setLong(3, recipientId
.id());
748 statement
.executeUpdate();
752 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
753 logger
.debug("Migrating legacy recipients to database");
754 long start
= System
.nanoTime();
757 INSERT INTO %s (_id, number, aci)
760 ).formatted(TABLE_RECIPIENT
);
761 try (final var connection
= database
.getConnection()) {
762 connection
.setAutoCommit(false);
763 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
764 statement
.executeUpdate();
766 try (final var statement
= connection
.prepareStatement(sql
)) {
767 for (final var recipient
: recipients
.values()) {
768 statement
.setLong(1, recipient
.getRecipientId().id());
769 statement
.setString(2, recipient
.getAddress().number().orElse(null));
770 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
771 statement
.executeUpdate();
774 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
776 for (final var recipient
: recipients
.values()) {
777 if (recipient
.getContact() != null) {
778 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
780 if (recipient
.getProfile() != null) {
781 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
783 if (recipient
.getProfileKey() != null) {
784 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
786 if (recipient
.getExpiringProfileKeyCredential() != null) {
787 storeExpiringProfileKeyCredential(connection
,
788 recipient
.getRecipientId(),
789 recipient
.getExpiringProfileKeyCredential());
793 } catch (SQLException e
) {
794 throw new RuntimeException("Failed update recipient store", e
);
796 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
799 long getActualRecipientId(long recipientId
) {
800 while (recipientsMerged
.containsKey(recipientId
)) {
801 final var newRecipientId
= recipientsMerged
.get(recipientId
);
802 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
803 recipientId
= newRecipientId
;
808 public void storeContact(
809 final Connection connection
, final RecipientId recipientId
, final Contact contact
810 ) throws SQLException
{
814 SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?
817 ).formatted(TABLE_RECIPIENT
);
818 try (final var statement
= connection
.prepareStatement(sql
)) {
819 statement
.setString(1, contact
== null ?
null : contact
.givenName());
820 statement
.setString(2, contact
== null ?
null : contact
.familyName());
821 statement
.setString(3, contact
== null ?
null : contact
.nickName());
822 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
823 statement
.setLong(5, contact
== null ?
0 : contact
.muteUntil());
824 statement
.setBoolean(6, contact
!= null && contact
.hideStory());
825 statement
.setBoolean(7, contact
!= null && contact
.isProfileSharingEnabled());
826 statement
.setString(8, contact
== null ?
null : contact
.color());
827 statement
.setBoolean(9, contact
!= null && contact
.isBlocked());
828 statement
.setBoolean(10, contact
!= null && contact
.isArchived());
829 if (contact
== null || contact
.unregisteredTimestamp() == null) {
830 statement
.setNull(11, Types
.INTEGER
);
832 statement
.setLong(11, contact
.unregisteredTimestamp());
834 statement
.setLong(12, recipientId
.id());
835 statement
.executeUpdate();
837 if (contact
!= null && contact
.unregisteredTimestamp() != null) {
838 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
840 rotateStorageId(connection
, recipientId
);
843 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
844 final Connection connection
, final List
<StorageId
> storageIds
845 ) throws SQLException
{
849 SET storage_id = NULL
850 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
852 ).formatted(TABLE_RECIPIENT
);
854 try (final var statement
= connection
.prepareStatement(sql
)) {
855 for (final var storageId
: storageIds
) {
856 statement
.setBytes(1, storageId
.getRaw());
857 count
+= statement
.executeUpdate();
863 public void markNeedsPniSignature(final RecipientId recipientId
, final boolean value
) {
864 logger
.debug("Marking {} numbers as need pni signature = {}", recipientId
, value
);
865 try (final var connection
= database
.getConnection()) {
869 SET needs_pni_signature = ?
872 ).formatted(TABLE_RECIPIENT
);
873 try (final var statement
= connection
.prepareStatement(sql
)) {
874 statement
.setBoolean(1, value
);
875 statement
.setLong(2, recipientId
.id());
876 statement
.executeUpdate();
878 } catch (SQLException e
) {
879 throw new RuntimeException("Failed update recipient store", e
);
883 public boolean needsPniSignature(final RecipientId recipientId
) {
884 try (final var connection
= database
.getConnection()) {
887 SELECT needs_pni_signature
891 ).formatted(TABLE_RECIPIENT
);
892 try (final var statement
= connection
.prepareStatement(sql
)) {
893 statement
.setLong(1, recipientId
.id());
894 return Utils
.executeQuerySingleRow(statement
, resultSet
-> resultSet
.getBoolean("needs_pni_signature"));
896 } catch (SQLException e
) {
897 throw new RuntimeException("Failed read recipient store", e
);
901 public void markUnregistered(final Set
<String
> unregisteredUsers
) {
902 logger
.debug("Marking {} numbers as unregistered", unregisteredUsers
.size());
903 try (final var connection
= database
.getConnection()) {
904 connection
.setAutoCommit(false);
905 for (final var number
: unregisteredUsers
) {
906 final var recipient
= findByNumber(connection
, number
);
907 if (recipient
.isPresent()) {
908 final var recipientId
= recipient
.get().id();
909 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
913 } catch (SQLException e
) {
914 throw new RuntimeException("Failed update recipient store", e
);
918 private void markUnregisteredAndSplitIfNecessary(
919 final Connection connection
, final RecipientId recipientId
920 ) throws SQLException
{
921 markUnregistered(connection
, recipientId
);
922 final var address
= resolveRecipientAddress(connection
, recipientId
);
923 if (address
.aci().isPresent() && address
.pni().isPresent()) {
924 final var numberAddress
= new RecipientAddress(address
.pni().get(), address
.number().orElse(null));
925 updateRecipientAddress(connection
, recipientId
, address
.removeIdentifiersFrom(numberAddress
));
926 addNewRecipient(connection
, numberAddress
);
930 private void markRegistered(
931 final Connection connection
, final RecipientId recipientId
932 ) throws SQLException
{
936 SET unregistered_timestamp = NULL
939 ).formatted(TABLE_RECIPIENT
);
940 try (final var statement
= connection
.prepareStatement(sql
)) {
941 statement
.setLong(1, recipientId
.id());
942 statement
.executeUpdate();
946 private void markUnregistered(
947 final Connection connection
, final RecipientId recipientId
948 ) throws SQLException
{
952 SET unregistered_timestamp = ?
953 WHERE _id = ? AND unregistered_timestamp IS NULL
955 ).formatted(TABLE_RECIPIENT
);
956 try (final var statement
= connection
.prepareStatement(sql
)) {
957 statement
.setLong(1, System
.currentTimeMillis());
958 statement
.setLong(2, recipientId
.id());
959 statement
.executeUpdate();
963 private void storeExpiringProfileKeyCredential(
964 final Connection connection
,
965 final RecipientId recipientId
,
966 final ExpiringProfileKeyCredential profileKeyCredential
967 ) throws SQLException
{
971 SET profile_key_credential = ?
974 ).formatted(TABLE_RECIPIENT
);
975 try (final var statement
= connection
.prepareStatement(sql
)) {
976 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
977 statement
.setLong(2, recipientId
.id());
978 statement
.executeUpdate();
982 public void storeProfile(
983 final Connection connection
, final RecipientId recipientId
, final Profile profile
984 ) throws SQLException
{
988 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 = ?
991 ).formatted(TABLE_RECIPIENT
);
992 try (final var statement
= connection
.prepareStatement(sql
)) {
993 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
994 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
995 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
996 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
997 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
998 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
999 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
1000 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
1001 statement
.setString(9,
1004 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
1005 statement
.setLong(10, recipientId
.id());
1006 statement
.executeUpdate();
1008 rotateStorageId(connection
, recipientId
);
1011 private void storeProfileKey(
1012 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
1013 ) throws SQLException
{
1014 if (profileKey
!= null) {
1015 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
1016 if (profileKey
.equals(recipientProfileKey
)) {
1017 final var recipientProfile
= getProfile(connection
, recipientId
);
1018 if (recipientProfile
== null || (
1019 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
1020 && recipientProfile
.getUnidentifiedAccessMode()
1021 != Profile
.UnidentifiedAccessMode
.DISABLED
1031 SET profile_key = ?, profile_key_credential = NULL%s
1034 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
1035 try (final var statement
= connection
.prepareStatement(sql
)) {
1036 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
1037 statement
.setLong(2, recipientId
.id());
1038 statement
.executeUpdate();
1040 rotateStorageId(connection
, recipientId
);
1043 private RecipientAddress
resolveRecipientAddress(
1044 final Connection connection
, final RecipientId recipientId
1045 ) throws SQLException
{
1048 SELECT r.number, r.aci, r.pni, r.username
1052 ).formatted(TABLE_RECIPIENT
);
1053 try (final var statement
= connection
.prepareStatement(sql
)) {
1054 statement
.setLong(1, recipientId
.id());
1055 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
1059 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1060 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1061 try (final var connection
= database
.getConnection()) {
1062 connection
.setAutoCommit(false);
1063 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1064 connection
.commit();
1065 } catch (SQLException e
) {
1066 throw new RuntimeException("Failed update recipient store", e
);
1069 if (!pair
.second().isEmpty()) {
1070 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1071 try (final var connection
= database
.getConnection()) {
1072 connection
.setAutoCommit(false);
1073 mergeRecipients(connection
, pair
.first(), pair
.second());
1074 connection
.commit();
1075 } catch (SQLException e
) {
1076 throw new RuntimeException("Failed update recipient store", e
);
1079 return pair
.first();
1082 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1083 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1084 ) throws SQLException
{
1085 if (address
.hasSingleIdentifier() || (
1086 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1088 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1090 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1091 markRegistered(connection
, pair
.first());
1093 for (final var toBeMergedRecipientId
: pair
.second()) {
1094 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1100 private void mergeRecipients(
1101 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1102 ) throws SQLException
{
1103 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1104 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1105 deleteRecipient(connection
, toBeMergedRecipientId
);
1106 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1110 private RecipientId
resolveRecipientLocked(
1111 Connection connection
, RecipientAddress address
1112 ) throws SQLException
{
1113 final var byAci
= address
.aci().isEmpty()
1114 ? Optional
.<RecipientWithAddress
>empty()
1115 : findByServiceId(connection
, address
.aci().get());
1117 if (byAci
.isPresent()) {
1118 return byAci
.get().id();
1121 final var byPni
= address
.pni().isEmpty()
1122 ? Optional
.<RecipientWithAddress
>empty()
1123 : findByServiceId(connection
, address
.pni().get());
1125 if (byPni
.isPresent()) {
1126 return byPni
.get().id();
1129 final var byNumber
= address
.number().isEmpty()
1130 ? Optional
.<RecipientWithAddress
>empty()
1131 : findByNumber(connection
, address
.number().get());
1133 if (byNumber
.isPresent()) {
1134 return byNumber
.get().id();
1137 logger
.debug("Got new recipient, both serviceId and number are unknown");
1139 if (address
.serviceId().isEmpty()) {
1140 return addNewRecipient(connection
, address
);
1143 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1146 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1147 final var recipient
= findByServiceId(connection
, serviceId
);
1149 if (recipient
.isEmpty()) {
1150 logger
.debug("Got new recipient, serviceId is unknown");
1151 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1154 return recipient
.get().id();
1157 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1158 final var recipient
= findByNumber(connection
, number
);
1160 if (recipient
.isEmpty()) {
1161 logger
.debug("Got new recipient, number is unknown");
1162 return addNewRecipient(connection
, new RecipientAddress(number
));
1165 return recipient
.get().id();
1168 private RecipientId
addNewRecipient(
1169 final Connection connection
, final RecipientAddress address
1170 ) throws SQLException
{
1173 INSERT INTO %s (number, aci, pni, username)
1177 ).formatted(TABLE_RECIPIENT
);
1178 try (final var statement
= connection
.prepareStatement(sql
)) {
1179 statement
.setString(1, address
.number().orElse(null));
1180 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1181 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1182 statement
.setString(4, address
.username().orElse(null));
1183 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1184 if (generatedKey
.isPresent()) {
1185 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1186 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1189 throw new RuntimeException("Failed to add new recipient to database");
1194 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1195 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1199 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1202 ).formatted(TABLE_RECIPIENT
);
1203 try (final var statement
= connection
.prepareStatement(sql
)) {
1204 statement
.setLong(1, recipientId
.id());
1205 statement
.executeUpdate();
1209 private void updateRecipientAddress(
1210 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1211 ) throws SQLException
{
1212 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1216 SET number = ?, aci = ?, pni = ?, username = ?
1219 ).formatted(TABLE_RECIPIENT
);
1220 try (final var statement
= connection
.prepareStatement(sql
)) {
1221 statement
.setString(1, address
.number().orElse(null));
1222 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1223 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1224 statement
.setString(4, address
.username().orElse(null));
1225 statement
.setLong(5, recipientId
.id());
1226 statement
.executeUpdate();
1228 rotateStorageId(connection
, recipientId
);
1231 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1237 ).formatted(TABLE_RECIPIENT
);
1238 try (final var statement
= connection
.prepareStatement(sql
)) {
1239 statement
.setLong(1, recipientId
.id());
1240 statement
.executeUpdate();
1244 private void mergeRecipientsLocked(
1245 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1246 ) throws SQLException
{
1247 final var contact
= getContact(connection
, recipientId
);
1248 if (contact
== null) {
1249 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1250 storeContact(connection
, recipientId
, toBeMergedContact
);
1253 final var profileKey
= getProfileKey(connection
, recipientId
);
1254 if (profileKey
== null) {
1255 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1256 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1259 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1260 if (profileKeyCredential
== null) {
1261 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1262 toBeMergedRecipientId
);
1263 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1266 final var profile
= getProfile(connection
, recipientId
);
1267 if (profile
== null) {
1268 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1269 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1272 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1275 private Optional
<RecipientWithAddress
> findByNumber(
1276 final Connection connection
, final String number
1277 ) throws SQLException
{
1279 SELECT r._id, r.number, r.aci, r.pni, r.username
1283 """.formatted(TABLE_RECIPIENT
);
1284 try (final var statement
= connection
.prepareStatement(sql
)) {
1285 statement
.setString(1, number
);
1286 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1290 private Optional
<RecipientWithAddress
> findByUsername(
1291 final Connection connection
, final String username
1292 ) throws SQLException
{
1294 SELECT r._id, r.number, r.aci, r.pni, r.username
1296 WHERE r.username = ?
1298 """.formatted(TABLE_RECIPIENT
);
1299 try (final var statement
= connection
.prepareStatement(sql
)) {
1300 statement
.setString(1, username
);
1301 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1305 private Optional
<RecipientWithAddress
> findByServiceId(
1306 final Connection connection
, final ServiceId serviceId
1307 ) throws SQLException
{
1308 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1309 if (recipientWithAddress
.isPresent()) {
1310 return recipientWithAddress
;
1313 SELECT r._id, r.number, r.aci, r.pni, r.username
1317 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1318 try (final var statement
= connection
.prepareStatement(sql
)) {
1319 statement
.setString(1, serviceId
.toString());
1320 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1321 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1322 return recipientWithAddress
;
1326 private Set
<RecipientWithAddress
> findAllByAddress(
1327 final Connection connection
, final RecipientAddress address
1328 ) throws SQLException
{
1330 SELECT r._id, r.number, r.aci, r.pni, r.username
1336 """.formatted(TABLE_RECIPIENT
);
1337 try (final var statement
= connection
.prepareStatement(sql
)) {
1338 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1339 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1340 statement
.setString(3, address
.number().orElse(null));
1341 statement
.setString(4, address
.username().orElse(null));
1342 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1343 .collect(Collectors
.toSet());
1347 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1350 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
1352 WHERE r._id = ? AND (%s)
1354 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1355 try (final var statement
= connection
.prepareStatement(sql
)) {
1356 statement
.setLong(1, recipientId
.id());
1357 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1361 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1362 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1363 if (recipientId
.equals(selfRecipientId
)) {
1364 return selfProfileKeyProvider
.getSelfProfileKey();
1368 SELECT r.profile_key
1372 ).formatted(TABLE_RECIPIENT
);
1373 try (final var statement
= connection
.prepareStatement(sql
)) {
1374 statement
.setLong(1, recipientId
.id());
1375 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1379 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1380 final Connection connection
, final RecipientId recipientId
1381 ) throws SQLException
{
1384 SELECT r.profile_key_credential
1388 ).formatted(TABLE_RECIPIENT
);
1389 try (final var statement
= connection
.prepareStatement(sql
)) {
1390 statement
.setLong(1, recipientId
.id());
1391 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1396 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1399 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
1401 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1403 ).formatted(TABLE_RECIPIENT
);
1404 try (final var statement
= connection
.prepareStatement(sql
)) {
1405 statement
.setLong(1, recipientId
.id());
1406 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1410 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1411 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrNull
);
1412 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrNull
);
1413 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1414 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1415 return new RecipientAddress(aci
, pni
, number
, username
);
1418 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1419 return new RecipientId(resultSet
.getLong("_id"), this);
1422 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1423 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1424 getRecipientAddressFromResultSet(resultSet
));
1427 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1428 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1429 getRecipientAddressFromResultSet(resultSet
),
1430 getContactFromResultSet(resultSet
),
1431 getProfileKeyFromResultSet(resultSet
),
1432 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1433 getProfileFromResultSet(resultSet
),
1434 getStorageRecordFromResultSet(resultSet
));
1437 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1438 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1439 return new Contact(resultSet
.getString("given_name"),
1440 resultSet
.getString("family_name"),
1441 resultSet
.getString("nick_name"),
1442 resultSet
.getString("nick_name_given_name"),
1443 resultSet
.getString("nick_name_family_name"),
1444 resultSet
.getString("note"),
1445 resultSet
.getString("color"),
1446 resultSet
.getInt("expiration_time"),
1447 resultSet
.getLong("mute_until"),
1448 resultSet
.getBoolean("hide_story"),
1449 resultSet
.getBoolean("blocked"),
1450 resultSet
.getBoolean("archived"),
1451 resultSet
.getBoolean("profile_sharing"),
1452 resultSet
.getBoolean("hidden"),
1453 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1456 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1457 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1458 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1459 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1460 resultSet
.getString("profile_given_name"),
1461 resultSet
.getString("profile_family_name"),
1462 resultSet
.getString("profile_about"),
1463 resultSet
.getString("profile_about_emoji"),
1464 resultSet
.getString("profile_avatar_url_path"),
1465 resultSet
.getBytes("profile_mobile_coin_address"),
1466 profileUnidentifiedAccessMode
== null
1467 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1468 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1469 profileCapabilities
== null
1471 : Arrays
.stream(profileCapabilities
.split(","))
1472 .map(Profile
.Capability
::valueOfOrNull
)
1473 .filter(Objects
::nonNull
)
1474 .collect(Collectors
.toSet()));
1477 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1478 final var profileKey
= resultSet
.getBytes("profile_key");
1480 if (profileKey
== null) {
1484 return new ProfileKey(profileKey
);
1485 } catch (InvalidInputException ignored
) {
1490 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1491 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1493 if (profileKeyCredential
== null) {
1497 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1498 } catch (Throwable ignored
) {
1503 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1504 final var storageId
= resultSet
.getBytes("storage_id");
1505 return StorageId
.forContact(storageId
);
1508 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1509 return resultSet
.getBytes("storage_record");
1512 public interface RecipientMergeHandler
{
1514 void mergeRecipients(
1515 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1516 ) throws SQLException
;
1519 private class HelperStore
implements MergeRecipientHelper
.Store
{
1521 private final Connection connection
;
1523 public HelperStore(final Connection connection
) {
1524 this.connection
= connection
;
1528 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1529 return RecipientStore
.this.findAllByAddress(connection
, address
);
1533 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1534 return RecipientStore
.this.addNewRecipient(connection
, address
);
1538 public void updateRecipientAddress(
1539 final RecipientId recipientId
, final RecipientAddress address
1540 ) throws SQLException
{
1541 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1545 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1546 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);