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,
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 final RecipientId recipientId
;
168 try (final var connection
= database
.getConnection()) {
169 connection
.setAutoCommit(false);
170 recipientId
= resolveRecipientLocked(connection
, number
);
172 } catch (SQLException e
) {
173 throw new RuntimeException("Failed read recipient store", e
);
179 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
180 try (final var connection
= database
.getConnection()) {
181 connection
.setAutoCommit(false);
182 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
183 if (recipientWithAddress
!= null) {
184 return recipientWithAddress
.id();
186 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
189 } catch (SQLException e
) {
190 throw new RuntimeException("Failed read recipient store", e
);
195 * Should only be used for recipientIds from the database.
196 * Where the foreign key relations ensure a valid recipientId.
199 public RecipientId
create(final long recipientId
) {
200 return new RecipientId(recipientId
, this);
203 public RecipientId
resolveRecipientByNumber(
204 final String number
, Supplier
<ServiceId
> serviceIdSupplier
205 ) throws UnregisteredRecipientException
{
206 final Optional
<RecipientWithAddress
> byNumber
;
207 try (final var connection
= database
.getConnection()) {
208 byNumber
= findByNumber(connection
, number
);
209 } catch (SQLException e
) {
210 throw new RuntimeException("Failed read from recipient store", e
);
212 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
213 final var serviceId
= serviceIdSupplier
.get();
214 if (serviceId
== null) {
215 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
219 return resolveRecipient(serviceId
);
221 return byNumber
.get().id();
224 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
225 final Optional
<RecipientWithAddress
> byNumber
;
226 try (final var connection
= database
.getConnection()) {
227 byNumber
= findByNumber(connection
, number
);
228 } catch (SQLException e
) {
229 throw new RuntimeException("Failed read from recipient store", e
);
231 return byNumber
.map(RecipientWithAddress
::id
);
234 public RecipientId
resolveRecipientByUsername(
235 final String username
, Supplier
<ACI
> aciSupplier
236 ) throws UnregisteredRecipientException
{
237 final Optional
<RecipientWithAddress
> byUsername
;
238 try (final var connection
= database
.getConnection()) {
239 byUsername
= findByUsername(connection
, username
);
240 } catch (SQLException e
) {
241 throw new RuntimeException("Failed read from recipient store", e
);
243 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
244 final var aci
= aciSupplier
.get();
246 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
251 return resolveRecipientTrusted(aci
, username
);
253 return byUsername
.get().id();
256 public RecipientId
resolveRecipient(RecipientAddress address
) {
257 final RecipientId recipientId
;
258 try (final var connection
= database
.getConnection()) {
259 connection
.setAutoCommit(false);
260 recipientId
= resolveRecipientLocked(connection
, address
);
262 } catch (SQLException e
) {
263 throw new RuntimeException("Failed read recipient store", e
);
268 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
269 return resolveRecipientLocked(connection
, address
);
273 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
274 return resolveRecipientTrusted(address
, true);
278 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
279 return resolveRecipientTrusted(address
, false);
282 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
283 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
284 if (!pair
.second().isEmpty()) {
285 mergeRecipients(connection
, pair
.first(), pair
.second());
291 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
292 return resolveRecipientTrusted(new RecipientAddress(address
));
296 public RecipientId
resolveRecipientTrusted(
297 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
299 return resolveRecipientTrusted(new RecipientAddress(aci
, pni
, number
, Optional
.empty()));
303 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
304 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
308 public void storeContact(RecipientId recipientId
, final Contact contact
) {
309 try (final var connection
= database
.getConnection()) {
310 storeContact(connection
, recipientId
, contact
);
311 } catch (SQLException e
) {
312 throw new RuntimeException("Failed update recipient store", e
);
317 public Contact
getContact(RecipientId recipientId
) {
318 try (final var connection
= database
.getConnection()) {
319 return getContact(connection
, recipientId
);
320 } catch (SQLException e
) {
321 throw new RuntimeException("Failed read from recipient store", e
);
326 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
329 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
331 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
333 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
334 try (final var connection
= database
.getConnection()) {
335 try (final var statement
= connection
.prepareStatement(sql
)) {
336 try (var result
= Utils
.executeQueryForStream(statement
,
337 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
338 getContactFromResultSet(resultSet
)))) {
339 return result
.toList();
342 } catch (SQLException e
) {
343 throw new RuntimeException("Failed read from recipient store", e
);
347 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
351 r.number, r.aci, r.pni, r.username,
352 r.profile_key, r.profile_key_credential,
353 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,
354 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,
359 ).formatted(TABLE_RECIPIENT
);
360 try (final var statement
= connection
.prepareStatement(sql
)) {
361 statement
.setLong(1, recipientId
.id());
362 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
366 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
370 r.number, r.aci, r.pni, r.username,
371 r.profile_key, r.profile_key_credential,
372 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,
373 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,
376 WHERE r.storage_id = ?
378 ).formatted(TABLE_RECIPIENT
);
379 try (final var statement
= connection
.prepareStatement(sql
)) {
380 statement
.setBytes(1, storageId
.getRaw());
381 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
385 public List
<Recipient
> getRecipients(
386 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
388 final var sqlWhere
= new ArrayList
<String
>();
390 sqlWhere
.add("r.unregistered_timestamp IS NULL");
391 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
392 sqlWhere
.add("r.hidden = FALSE");
394 if (blocked
.isPresent()) {
395 sqlWhere
.add("r.blocked = ?");
397 if (!recipientIds
.isEmpty()) {
398 final var recipientIdsCommaSeparated
= recipientIds
.stream()
399 .map(recipientId
-> String
.valueOf(recipientId
.id()))
400 .collect(Collectors
.joining(","));
401 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
406 r.number, r.aci, r.pni, r.username,
407 r.profile_key, r.profile_key_credential,
408 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,
409 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,
412 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
414 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
415 final var selfAddress
= selfAddressProvider
.getSelfAddress();
416 try (final var connection
= database
.getConnection()) {
417 try (final var statement
= connection
.prepareStatement(sql
)) {
418 if (blocked
.isPresent()) {
419 statement
.setBoolean(1, blocked
.get());
421 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
422 return result
.filter(r
-> name
.isEmpty() || (
423 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
424 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
425 if (r
.getAddress().matches(selfAddress
)) {
426 return Recipient
.newBuilder(r
)
427 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
434 } catch (SQLException e
) {
435 throw new RuntimeException("Failed read from recipient store", e
);
439 public Set
<String
> getAllNumbers() {
444 WHERE r.number IS NOT NULL
446 ).formatted(TABLE_RECIPIENT
);
447 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
448 try (final var connection
= database
.getConnection()) {
449 try (final var statement
= connection
.prepareStatement(sql
)) {
450 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
451 .filter(Objects
::nonNull
)
452 .filter(n
-> !n
.equals(selfNumber
))
457 } catch (NumberFormatException e
) {
461 .collect(Collectors
.toSet());
463 } catch (SQLException e
) {
464 throw new RuntimeException("Failed read from recipient store", e
);
468 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
471 SELECT r.aci, r.profile_key
473 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
475 ).formatted(TABLE_RECIPIENT
);
476 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
477 try (final var connection
= database
.getConnection()) {
478 try (final var statement
= connection
.prepareStatement(sql
)) {
479 return Utils
.executeQueryForStream(statement
, resultSet
-> {
480 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
481 if (aci
.equals(selfAci
)) {
482 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
484 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
485 return new Pair
<>(aci
, profileKey
);
486 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
488 } catch (SQLException e
) {
489 throw new RuntimeException("Failed read from recipient store", e
);
493 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
498 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
500 ).formatted(TABLE_RECIPIENT
);
501 try (final var statement
= connection
.prepareStatement(sql
)) {
502 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
506 public void setMissingStorageIds() {
507 final var selectSql
= (
511 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
513 ).formatted(TABLE_RECIPIENT
);
514 final var updateSql
= (
520 ).formatted(TABLE_RECIPIENT
);
521 try (final var connection
= database
.getConnection()) {
522 connection
.setAutoCommit(false);
523 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
524 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
526 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
527 for (final var recipientId
: recipientIds
) {
528 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
529 updateStmt
.setLong(2, recipientId
.id());
530 updateStmt
.executeUpdate();
535 } catch (SQLException e
) {
536 throw new RuntimeException("Failed update recipient store", e
);
541 public void deleteContact(RecipientId recipientId
) {
542 storeContact(recipientId
, null);
545 public void deleteRecipientData(RecipientId recipientId
) {
546 logger
.debug("Deleting recipient data for {}", recipientId
);
547 try (final var connection
= database
.getConnection()) {
548 connection
.setAutoCommit(false);
549 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
550 storeContact(connection
, recipientId
, null);
551 storeProfile(connection
, recipientId
, null);
552 storeProfileKey(connection
, recipientId
, null, false);
553 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
554 deleteRecipient(connection
, recipientId
);
556 } catch (SQLException e
) {
557 throw new RuntimeException("Failed update recipient store", e
);
562 public Profile
getProfile(final RecipientId recipientId
) {
563 try (final var connection
= database
.getConnection()) {
564 return getProfile(connection
, recipientId
);
565 } catch (SQLException e
) {
566 throw new RuntimeException("Failed read from recipient store", e
);
571 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
572 try (final var connection
= database
.getConnection()) {
573 return getProfileKey(connection
, recipientId
);
574 } catch (SQLException e
) {
575 throw new RuntimeException("Failed read from recipient store", e
);
580 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
581 try (final var connection
= database
.getConnection()) {
582 return getExpiringProfileKeyCredential(connection
, recipientId
);
583 } catch (SQLException e
) {
584 throw new RuntimeException("Failed read from recipient store", e
);
589 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
590 try (final var connection
= database
.getConnection()) {
591 storeProfile(connection
, recipientId
, profile
);
592 } catch (SQLException e
) {
593 throw new RuntimeException("Failed update recipient store", e
);
598 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
599 try (final var connection
= database
.getConnection()) {
600 storeProfileKey(connection
, recipientId
, profileKey
);
601 } catch (SQLException e
) {
602 throw new RuntimeException("Failed update recipient store", e
);
606 public void storeProfileKey(
607 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
608 ) throws SQLException
{
609 storeProfileKey(connection
, recipientId
, profileKey
, true);
613 public void storeExpiringProfileKeyCredential(
614 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
616 try (final var connection
= database
.getConnection()) {
617 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
618 } catch (SQLException e
) {
619 throw new RuntimeException("Failed update recipient store", e
);
623 public void rotateSelfStorageId() {
624 try (final var connection
= database
.getConnection()) {
625 rotateSelfStorageId(connection
);
626 } catch (SQLException e
) {
627 throw new RuntimeException("Failed update recipient store", e
);
631 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
632 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
633 rotateStorageId(connection
, selfRecipientId
);
636 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
637 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
638 return rotateStorageId(connection
, selfRecipientId
);
641 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
644 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)
645 """.formatted(TABLE_RECIPIENT
);
646 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
647 try (final var statement
= connection
.prepareStatement(sql
)) {
648 statement
.setLong(1, selfRecipientId
.id());
649 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
653 public void updateStorageId(
654 Connection connection
, RecipientId recipientId
, StorageId storageId
655 ) throws SQLException
{
662 ).formatted(TABLE_RECIPIENT
);
663 try (final var statement
= connection
.prepareStatement(sql
)) {
664 statement
.setBytes(1, storageId
.getRaw());
665 statement
.setLong(2, recipientId
.id());
666 statement
.executeUpdate();
670 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
677 ).formatted(TABLE_RECIPIENT
);
678 try (final var statement
= connection
.prepareStatement(sql
)) {
679 for (final var entry
: storageIdMap
.entrySet()) {
680 statement
.setBytes(1, entry
.getValue().getRaw());
681 statement
.setLong(2, entry
.getKey().id());
682 statement
.executeUpdate();
687 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
688 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
689 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
692 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
695 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
696 """.formatted(TABLE_RECIPIENT
);
697 try (final var statement
= connection
.prepareStatement(sql
)) {
698 statement
.setLong(1, recipientId
.id());
699 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
700 if (storageId
.isPresent()) {
701 return storageId
.get();
704 return rotateStorageId(connection
, recipientId
);
707 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
708 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
709 updateStorageId(connection
, recipientId
, newStorageId
);
713 public void storeStorageRecord(
714 final Connection connection
,
715 final RecipientId recipientId
,
716 final StorageId storageId
,
717 final byte[] storageRecord
718 ) throws SQLException
{
719 final var deleteSql
= (
722 SET storage_id = NULL
725 ).formatted(TABLE_RECIPIENT
);
726 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
727 statement
.setBytes(1, storageId
.getRaw());
728 statement
.executeUpdate();
730 final var insertSql
= (
733 SET storage_id = ?, storage_record = ?
736 ).formatted(TABLE_RECIPIENT
);
737 try (final var statement
= connection
.prepareStatement(insertSql
)) {
738 statement
.setBytes(1, storageId
.getRaw());
739 if (storageRecord
== null) {
740 statement
.setNull(2, Types
.BLOB
);
742 statement
.setBytes(2, storageRecord
);
744 statement
.setLong(3, recipientId
.id());
745 statement
.executeUpdate();
749 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
750 logger
.debug("Migrating legacy recipients to database");
751 long start
= System
.nanoTime();
754 INSERT INTO %s (_id, number, aci)
757 ).formatted(TABLE_RECIPIENT
);
758 try (final var connection
= database
.getConnection()) {
759 connection
.setAutoCommit(false);
760 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
761 statement
.executeUpdate();
763 try (final var statement
= connection
.prepareStatement(sql
)) {
764 for (final var recipient
: recipients
.values()) {
765 statement
.setLong(1, recipient
.getRecipientId().id());
766 statement
.setString(2, recipient
.getAddress().number().orElse(null));
767 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
768 statement
.executeUpdate();
771 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
773 for (final var recipient
: recipients
.values()) {
774 if (recipient
.getContact() != null) {
775 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
777 if (recipient
.getProfile() != null) {
778 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
780 if (recipient
.getProfileKey() != null) {
781 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
783 if (recipient
.getExpiringProfileKeyCredential() != null) {
784 storeExpiringProfileKeyCredential(connection
,
785 recipient
.getRecipientId(),
786 recipient
.getExpiringProfileKeyCredential());
790 } catch (SQLException e
) {
791 throw new RuntimeException("Failed update recipient store", e
);
793 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
796 long getActualRecipientId(long recipientId
) {
797 while (recipientsMerged
.containsKey(recipientId
)) {
798 final var newRecipientId
= recipientsMerged
.get(recipientId
);
799 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
800 recipientId
= newRecipientId
;
805 public void storeContact(
806 final Connection connection
, final RecipientId recipientId
, final Contact contact
807 ) throws SQLException
{
811 SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?
814 ).formatted(TABLE_RECIPIENT
);
815 try (final var statement
= connection
.prepareStatement(sql
)) {
816 statement
.setString(1, contact
== null ?
null : contact
.givenName());
817 statement
.setString(2, contact
== null ?
null : contact
.familyName());
818 statement
.setString(3, contact
== null ?
null : contact
.nickName());
819 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
820 statement
.setLong(5, contact
== null ?
0 : contact
.muteUntil());
821 statement
.setBoolean(6, contact
!= null && contact
.hideStory());
822 statement
.setBoolean(7, contact
!= null && contact
.isProfileSharingEnabled());
823 statement
.setString(8, contact
== null ?
null : contact
.color());
824 statement
.setBoolean(9, contact
!= null && contact
.isBlocked());
825 statement
.setBoolean(10, contact
!= null && contact
.isArchived());
826 if (contact
== null || contact
.unregisteredTimestamp() == null) {
827 statement
.setNull(11, Types
.INTEGER
);
829 statement
.setLong(11, contact
.unregisteredTimestamp());
831 statement
.setLong(12, recipientId
.id());
832 statement
.executeUpdate();
834 if (contact
!= null && contact
.unregisteredTimestamp() != null) {
835 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
837 rotateStorageId(connection
, recipientId
);
840 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
841 final Connection connection
, final List
<StorageId
> storageIds
842 ) throws SQLException
{
846 SET storage_id = NULL
847 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
849 ).formatted(TABLE_RECIPIENT
);
851 try (final var statement
= connection
.prepareStatement(sql
)) {
852 for (final var storageId
: storageIds
) {
853 statement
.setBytes(1, storageId
.getRaw());
854 count
+= statement
.executeUpdate();
860 public void markNeedsPniSignature(final RecipientId recipientId
, final boolean value
) {
861 logger
.debug("Marking {} numbers as need pni signature = {}", recipientId
, value
);
862 try (final var connection
= database
.getConnection()) {
866 SET needs_pni_signature = ?
869 ).formatted(TABLE_RECIPIENT
);
870 try (final var statement
= connection
.prepareStatement(sql
)) {
871 statement
.setBoolean(1, value
);
872 statement
.setLong(2, recipientId
.id());
873 statement
.executeUpdate();
875 } catch (SQLException e
) {
876 throw new RuntimeException("Failed update recipient store", e
);
880 public boolean needsPniSignature(final RecipientId recipientId
) {
881 try (final var connection
= database
.getConnection()) {
884 SELECT needs_pni_signature
888 ).formatted(TABLE_RECIPIENT
);
889 try (final var statement
= connection
.prepareStatement(sql
)) {
890 statement
.setLong(1, recipientId
.id());
891 return Utils
.executeQuerySingleRow(statement
, resultSet
-> resultSet
.getBoolean("needs_pni_signature"));
893 } catch (SQLException e
) {
894 throw new RuntimeException("Failed read recipient store", e
);
898 public void markUnregistered(final Set
<String
> unregisteredUsers
) {
899 logger
.debug("Marking {} numbers as unregistered", unregisteredUsers
.size());
900 try (final var connection
= database
.getConnection()) {
901 connection
.setAutoCommit(false);
902 for (final var number
: unregisteredUsers
) {
903 final var recipient
= findByNumber(connection
, number
);
904 if (recipient
.isPresent()) {
905 final var recipientId
= recipient
.get().id();
906 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
910 } catch (SQLException e
) {
911 throw new RuntimeException("Failed update recipient store", e
);
915 private void markUnregisteredAndSplitIfNecessary(
916 final Connection connection
, final RecipientId recipientId
917 ) throws SQLException
{
918 markUnregistered(connection
, recipientId
);
919 final var address
= resolveRecipientAddress(connection
, recipientId
);
920 if (address
.aci().isPresent() && address
.pni().isPresent()) {
921 final var numberAddress
= new RecipientAddress(address
.pni().get(), address
.number().orElse(null));
922 updateRecipientAddress(connection
, recipientId
, address
.removeIdentifiersFrom(numberAddress
));
923 addNewRecipient(connection
, numberAddress
);
927 private void markRegistered(
928 final Connection connection
, final RecipientId recipientId
929 ) throws SQLException
{
933 SET unregistered_timestamp = NULL
936 ).formatted(TABLE_RECIPIENT
);
937 try (final var statement
= connection
.prepareStatement(sql
)) {
938 statement
.setLong(1, recipientId
.id());
939 statement
.executeUpdate();
943 private void markUnregistered(
944 final Connection connection
, final RecipientId recipientId
945 ) throws SQLException
{
949 SET unregistered_timestamp = ?
950 WHERE _id = ? AND unregistered_timestamp IS NULL
952 ).formatted(TABLE_RECIPIENT
);
953 try (final var statement
= connection
.prepareStatement(sql
)) {
954 statement
.setLong(1, System
.currentTimeMillis());
955 statement
.setLong(2, recipientId
.id());
956 statement
.executeUpdate();
960 private void storeExpiringProfileKeyCredential(
961 final Connection connection
,
962 final RecipientId recipientId
,
963 final ExpiringProfileKeyCredential profileKeyCredential
964 ) throws SQLException
{
968 SET profile_key_credential = ?
971 ).formatted(TABLE_RECIPIENT
);
972 try (final var statement
= connection
.prepareStatement(sql
)) {
973 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
974 statement
.setLong(2, recipientId
.id());
975 statement
.executeUpdate();
979 public void storeProfile(
980 final Connection connection
, final RecipientId recipientId
, final Profile profile
981 ) throws SQLException
{
985 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 = ?
988 ).formatted(TABLE_RECIPIENT
);
989 try (final var statement
= connection
.prepareStatement(sql
)) {
990 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
991 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
992 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
993 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
994 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
995 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
996 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
997 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
998 statement
.setString(9,
1001 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
1002 statement
.setLong(10, recipientId
.id());
1003 statement
.executeUpdate();
1005 rotateStorageId(connection
, recipientId
);
1008 private void storeProfileKey(
1009 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
1010 ) throws SQLException
{
1011 if (profileKey
!= null) {
1012 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
1013 if (profileKey
.equals(recipientProfileKey
)) {
1014 final var recipientProfile
= getProfile(connection
, recipientId
);
1015 if (recipientProfile
== null || (
1016 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
1017 && recipientProfile
.getUnidentifiedAccessMode()
1018 != Profile
.UnidentifiedAccessMode
.DISABLED
1028 SET profile_key = ?, profile_key_credential = NULL%s
1031 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
1032 try (final var statement
= connection
.prepareStatement(sql
)) {
1033 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
1034 statement
.setLong(2, recipientId
.id());
1035 statement
.executeUpdate();
1037 rotateStorageId(connection
, recipientId
);
1040 private RecipientAddress
resolveRecipientAddress(
1041 final Connection connection
, final RecipientId recipientId
1042 ) throws SQLException
{
1045 SELECT r.number, r.aci, r.pni, r.username
1049 ).formatted(TABLE_RECIPIENT
);
1050 try (final var statement
= connection
.prepareStatement(sql
)) {
1051 statement
.setLong(1, recipientId
.id());
1052 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
1056 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1057 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1058 try (final var connection
= database
.getConnection()) {
1059 connection
.setAutoCommit(false);
1060 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1061 connection
.commit();
1062 } catch (SQLException e
) {
1063 throw new RuntimeException("Failed update recipient store", e
);
1066 if (!pair
.second().isEmpty()) {
1067 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1068 try (final var connection
= database
.getConnection()) {
1069 connection
.setAutoCommit(false);
1070 mergeRecipients(connection
, pair
.first(), pair
.second());
1071 connection
.commit();
1072 } catch (SQLException e
) {
1073 throw new RuntimeException("Failed update recipient store", e
);
1076 return pair
.first();
1079 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1080 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1081 ) throws SQLException
{
1082 if (address
.hasSingleIdentifier() || (
1083 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1085 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1087 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1088 markRegistered(connection
, pair
.first());
1090 for (final var toBeMergedRecipientId
: pair
.second()) {
1091 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1097 private void mergeRecipients(
1098 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1099 ) throws SQLException
{
1100 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1101 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1102 deleteRecipient(connection
, toBeMergedRecipientId
);
1103 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1107 private RecipientId
resolveRecipientLocked(
1108 Connection connection
, RecipientAddress address
1109 ) throws SQLException
{
1110 final var byAci
= address
.aci().isEmpty()
1111 ? Optional
.<RecipientWithAddress
>empty()
1112 : findByServiceId(connection
, address
.aci().get());
1114 if (byAci
.isPresent()) {
1115 return byAci
.get().id();
1118 final var byPni
= address
.pni().isEmpty()
1119 ? Optional
.<RecipientWithAddress
>empty()
1120 : findByServiceId(connection
, address
.pni().get());
1122 if (byPni
.isPresent()) {
1123 return byPni
.get().id();
1126 final var byNumber
= address
.number().isEmpty()
1127 ? Optional
.<RecipientWithAddress
>empty()
1128 : findByNumber(connection
, address
.number().get());
1130 if (byNumber
.isPresent()) {
1131 return byNumber
.get().id();
1134 logger
.debug("Got new recipient, both serviceId and number are unknown");
1136 if (address
.serviceId().isEmpty()) {
1137 return addNewRecipient(connection
, address
);
1140 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1143 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1144 final var recipient
= findByServiceId(connection
, serviceId
);
1146 if (recipient
.isEmpty()) {
1147 logger
.debug("Got new recipient, serviceId is unknown");
1148 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1151 return recipient
.get().id();
1154 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1155 final var recipient
= findByNumber(connection
, number
);
1157 if (recipient
.isEmpty()) {
1158 logger
.debug("Got new recipient, number is unknown");
1159 return addNewRecipient(connection
, new RecipientAddress(number
));
1162 return recipient
.get().id();
1165 private RecipientId
addNewRecipient(
1166 final Connection connection
, final RecipientAddress address
1167 ) throws SQLException
{
1170 INSERT INTO %s (number, aci, pni, username)
1174 ).formatted(TABLE_RECIPIENT
);
1175 try (final var statement
= connection
.prepareStatement(sql
)) {
1176 statement
.setString(1, address
.number().orElse(null));
1177 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1178 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1179 statement
.setString(4, address
.username().orElse(null));
1180 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1181 if (generatedKey
.isPresent()) {
1182 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1183 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1186 throw new RuntimeException("Failed to add new recipient to database");
1191 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1192 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1196 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1199 ).formatted(TABLE_RECIPIENT
);
1200 try (final var statement
= connection
.prepareStatement(sql
)) {
1201 statement
.setLong(1, recipientId
.id());
1202 statement
.executeUpdate();
1206 private void updateRecipientAddress(
1207 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1208 ) throws SQLException
{
1209 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1213 SET number = ?, aci = ?, pni = ?, username = ?
1216 ).formatted(TABLE_RECIPIENT
);
1217 try (final var statement
= connection
.prepareStatement(sql
)) {
1218 statement
.setString(1, address
.number().orElse(null));
1219 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1220 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1221 statement
.setString(4, address
.username().orElse(null));
1222 statement
.setLong(5, recipientId
.id());
1223 statement
.executeUpdate();
1225 rotateStorageId(connection
, recipientId
);
1228 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1234 ).formatted(TABLE_RECIPIENT
);
1235 try (final var statement
= connection
.prepareStatement(sql
)) {
1236 statement
.setLong(1, recipientId
.id());
1237 statement
.executeUpdate();
1241 private void mergeRecipientsLocked(
1242 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1243 ) throws SQLException
{
1244 final var contact
= getContact(connection
, recipientId
);
1245 if (contact
== null) {
1246 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1247 storeContact(connection
, recipientId
, toBeMergedContact
);
1250 final var profileKey
= getProfileKey(connection
, recipientId
);
1251 if (profileKey
== null) {
1252 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1253 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1256 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1257 if (profileKeyCredential
== null) {
1258 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1259 toBeMergedRecipientId
);
1260 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1263 final var profile
= getProfile(connection
, recipientId
);
1264 if (profile
== null) {
1265 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1266 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1269 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1272 private Optional
<RecipientWithAddress
> findByNumber(
1273 final Connection connection
, final String number
1274 ) throws SQLException
{
1276 SELECT r._id, r.number, r.aci, r.pni, r.username
1280 """.formatted(TABLE_RECIPIENT
);
1281 try (final var statement
= connection
.prepareStatement(sql
)) {
1282 statement
.setString(1, number
);
1283 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1287 private Optional
<RecipientWithAddress
> findByUsername(
1288 final Connection connection
, final String username
1289 ) throws SQLException
{
1291 SELECT r._id, r.number, r.aci, r.pni, r.username
1293 WHERE r.username = ?
1295 """.formatted(TABLE_RECIPIENT
);
1296 try (final var statement
= connection
.prepareStatement(sql
)) {
1297 statement
.setString(1, username
);
1298 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1302 private Optional
<RecipientWithAddress
> findByServiceId(
1303 final Connection connection
, final ServiceId serviceId
1304 ) throws SQLException
{
1305 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1306 if (recipientWithAddress
.isPresent()) {
1307 return recipientWithAddress
;
1310 SELECT r._id, r.number, r.aci, r.pni, r.username
1314 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1315 try (final var statement
= connection
.prepareStatement(sql
)) {
1316 statement
.setString(1, serviceId
.toString());
1317 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1318 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1319 return recipientWithAddress
;
1323 private Set
<RecipientWithAddress
> findAllByAddress(
1324 final Connection connection
, final RecipientAddress address
1325 ) throws SQLException
{
1327 SELECT r._id, r.number, r.aci, r.pni, r.username
1333 """.formatted(TABLE_RECIPIENT
);
1334 try (final var statement
= connection
.prepareStatement(sql
)) {
1335 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1336 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1337 statement
.setString(3, address
.number().orElse(null));
1338 statement
.setString(4, address
.username().orElse(null));
1339 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1340 .collect(Collectors
.toSet());
1344 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1347 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
1349 WHERE r._id = ? AND (%s)
1351 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1352 try (final var statement
= connection
.prepareStatement(sql
)) {
1353 statement
.setLong(1, recipientId
.id());
1354 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1358 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1359 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1360 if (recipientId
.equals(selfRecipientId
)) {
1361 return selfProfileKeyProvider
.getSelfProfileKey();
1365 SELECT r.profile_key
1369 ).formatted(TABLE_RECIPIENT
);
1370 try (final var statement
= connection
.prepareStatement(sql
)) {
1371 statement
.setLong(1, recipientId
.id());
1372 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1376 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1377 final Connection connection
, final RecipientId recipientId
1378 ) throws SQLException
{
1381 SELECT r.profile_key_credential
1385 ).formatted(TABLE_RECIPIENT
);
1386 try (final var statement
= connection
.prepareStatement(sql
)) {
1387 statement
.setLong(1, recipientId
.id());
1388 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1393 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1396 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
1398 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1400 ).formatted(TABLE_RECIPIENT
);
1401 try (final var statement
= connection
.prepareStatement(sql
)) {
1402 statement
.setLong(1, recipientId
.id());
1403 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1407 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1408 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrNull
);
1409 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrNull
);
1410 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1411 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1412 return new RecipientAddress(aci
, pni
, number
, username
);
1415 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1416 return new RecipientId(resultSet
.getLong("_id"), this);
1419 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1420 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1421 getRecipientAddressFromResultSet(resultSet
));
1424 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1425 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1426 getRecipientAddressFromResultSet(resultSet
),
1427 getContactFromResultSet(resultSet
),
1428 getProfileKeyFromResultSet(resultSet
),
1429 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1430 getProfileFromResultSet(resultSet
),
1431 getStorageRecordFromResultSet(resultSet
));
1434 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1435 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1436 return new Contact(resultSet
.getString("given_name"),
1437 resultSet
.getString("family_name"),
1438 resultSet
.getString("nick_name"),
1439 resultSet
.getString("color"),
1440 resultSet
.getInt("expiration_time"),
1441 resultSet
.getLong("mute_until"),
1442 resultSet
.getBoolean("hide_story"),
1443 resultSet
.getBoolean("blocked"),
1444 resultSet
.getBoolean("archived"),
1445 resultSet
.getBoolean("profile_sharing"),
1446 resultSet
.getBoolean("hidden"),
1447 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1450 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1451 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1452 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1453 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1454 resultSet
.getString("profile_given_name"),
1455 resultSet
.getString("profile_family_name"),
1456 resultSet
.getString("profile_about"),
1457 resultSet
.getString("profile_about_emoji"),
1458 resultSet
.getString("profile_avatar_url_path"),
1459 resultSet
.getBytes("profile_mobile_coin_address"),
1460 profileUnidentifiedAccessMode
== null
1461 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1462 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1463 profileCapabilities
== null
1465 : Arrays
.stream(profileCapabilities
.split(","))
1466 .map(Profile
.Capability
::valueOfOrNull
)
1467 .filter(Objects
::nonNull
)
1468 .collect(Collectors
.toSet()));
1471 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1472 final var profileKey
= resultSet
.getBytes("profile_key");
1474 if (profileKey
== null) {
1478 return new ProfileKey(profileKey
);
1479 } catch (InvalidInputException ignored
) {
1484 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1485 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1487 if (profileKeyCredential
== null) {
1491 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1492 } catch (Throwable ignored
) {
1497 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1498 final var storageId
= resultSet
.getBytes("storage_id");
1499 return StorageId
.forContact(storageId
);
1502 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1503 return resultSet
.getBytes("storage_record");
1506 public interface RecipientMergeHandler
{
1508 void mergeRecipients(
1509 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1510 ) throws SQLException
;
1513 private class HelperStore
implements MergeRecipientHelper
.Store
{
1515 private final Connection connection
;
1517 public HelperStore(final Connection connection
) {
1518 this.connection
= connection
;
1522 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1523 return RecipientStore
.this.findAllByAddress(connection
, address
);
1527 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1528 return RecipientStore
.this.addNewRecipient(connection
, address
);
1532 public void updateRecipientAddress(
1533 final RecipientId recipientId
, final RecipientAddress address
1534 ) throws SQLException
{
1535 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1539 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1540 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);