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,
75 expiration_time INTEGER NOT NULL DEFAULT 0,
76 mute_until INTEGER NOT NULL DEFAULT 0,
77 blocked INTEGER NOT NULL DEFAULT FALSE,
78 archived INTEGER NOT NULL DEFAULT FALSE,
79 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
80 hide_story INTEGER NOT NULL DEFAULT FALSE,
81 hidden INTEGER NOT NULL DEFAULT FALSE,
83 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
84 profile_given_name TEXT,
85 profile_family_name TEXT,
87 profile_about_emoji TEXT,
88 profile_avatar_url_path TEXT,
89 profile_mobile_coin_address BLOB,
90 profile_unidentified_access_mode TEXT,
91 profile_capabilities TEXT
97 public RecipientStore(
98 final RecipientMergeHandler recipientMergeHandler
,
99 final SelfAddressProvider selfAddressProvider
,
100 final SelfProfileKeyProvider selfProfileKeyProvider
,
101 final Database database
103 this.recipientMergeHandler
= recipientMergeHandler
;
104 this.selfAddressProvider
= selfAddressProvider
;
105 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
106 this.database
= database
;
109 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
110 try (final var connection
= database
.getConnection()) {
111 return resolveRecipientAddress(connection
, recipientId
);
112 } catch (SQLException e
) {
113 throw new RuntimeException("Failed read from recipient store", e
);
117 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
122 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
124 ).formatted(TABLE_RECIPIENT
);
125 try (final var connection
= database
.getConnection()) {
126 try (final var statement
= connection
.prepareStatement(sql
)) {
127 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
128 return result
.toList();
131 } catch (SQLException e
) {
132 throw new RuntimeException("Failed read from recipient store", e
);
137 public RecipientId
resolveRecipient(final long rawRecipientId
) {
144 ).formatted(TABLE_RECIPIENT
);
145 try (final var connection
= database
.getConnection()) {
146 try (final var statement
= connection
.prepareStatement(sql
)) {
147 statement
.setLong(1, rawRecipientId
);
148 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
150 } catch (SQLException e
) {
151 throw new RuntimeException("Failed read from recipient store", e
);
156 public RecipientId
resolveRecipient(final String identifier
) {
157 final var serviceId
= ServiceId
.parseOrNull(identifier
);
158 if (serviceId
!= null) {
159 return resolveRecipient(serviceId
);
161 return resolveRecipientByNumber(identifier
);
165 private RecipientId
resolveRecipientByNumber(final String number
) {
166 final RecipientId recipientId
;
167 try (final var connection
= database
.getConnection()) {
168 connection
.setAutoCommit(false);
169 recipientId
= resolveRecipientLocked(connection
, number
);
171 } catch (SQLException e
) {
172 throw new RuntimeException("Failed read recipient store", e
);
178 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
179 try (final var connection
= database
.getConnection()) {
180 connection
.setAutoCommit(false);
181 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
182 if (recipientWithAddress
!= null) {
183 return recipientWithAddress
.id();
185 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
188 } catch (SQLException e
) {
189 throw new RuntimeException("Failed read recipient store", e
);
194 * Should only be used for recipientIds from the database.
195 * Where the foreign key relations ensure a valid recipientId.
198 public RecipientId
create(final long recipientId
) {
199 return new RecipientId(recipientId
, this);
202 public RecipientId
resolveRecipientByNumber(
203 final String number
, Supplier
<ServiceId
> serviceIdSupplier
204 ) throws UnregisteredRecipientException
{
205 final Optional
<RecipientWithAddress
> byNumber
;
206 try (final var connection
= database
.getConnection()) {
207 byNumber
= findByNumber(connection
, number
);
208 } catch (SQLException e
) {
209 throw new RuntimeException("Failed read from recipient store", e
);
211 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
212 final var serviceId
= serviceIdSupplier
.get();
213 if (serviceId
== null) {
214 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
218 return resolveRecipient(serviceId
);
220 return byNumber
.get().id();
223 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
224 final Optional
<RecipientWithAddress
> byNumber
;
225 try (final var connection
= database
.getConnection()) {
226 byNumber
= findByNumber(connection
, number
);
227 } catch (SQLException e
) {
228 throw new RuntimeException("Failed read from recipient store", e
);
230 return byNumber
.map(RecipientWithAddress
::id
);
233 public RecipientId
resolveRecipientByUsername(
234 final String username
, Supplier
<ACI
> aciSupplier
235 ) throws UnregisteredRecipientException
{
236 final Optional
<RecipientWithAddress
> byUsername
;
237 try (final var connection
= database
.getConnection()) {
238 byUsername
= findByUsername(connection
, username
);
239 } catch (SQLException e
) {
240 throw new RuntimeException("Failed read from recipient store", e
);
242 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
243 final var aci
= aciSupplier
.get();
245 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
250 return resolveRecipientTrusted(aci
, username
);
252 return byUsername
.get().id();
255 public RecipientId
resolveRecipient(RecipientAddress address
) {
256 final RecipientId recipientId
;
257 try (final var connection
= database
.getConnection()) {
258 connection
.setAutoCommit(false);
259 recipientId
= resolveRecipientLocked(connection
, address
);
261 } catch (SQLException e
) {
262 throw new RuntimeException("Failed read recipient store", e
);
267 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
268 return resolveRecipientLocked(connection
, address
);
272 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
273 return resolveRecipientTrusted(address
, true);
277 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
278 return resolveRecipientTrusted(address
, false);
281 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
282 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
283 if (!pair
.second().isEmpty()) {
284 mergeRecipients(connection
, pair
.first(), pair
.second());
290 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
291 return resolveRecipientTrusted(new RecipientAddress(address
));
295 public RecipientId
resolveRecipientTrusted(
296 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
298 return resolveRecipientTrusted(new RecipientAddress(aci
, pni
, number
, Optional
.empty()));
302 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
303 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
307 public void storeContact(RecipientId recipientId
, final Contact contact
) {
308 try (final var connection
= database
.getConnection()) {
309 storeContact(connection
, recipientId
, contact
);
310 } catch (SQLException e
) {
311 throw new RuntimeException("Failed update recipient store", e
);
316 public Contact
getContact(RecipientId recipientId
) {
317 try (final var connection
= database
.getConnection()) {
318 return getContact(connection
, recipientId
);
319 } catch (SQLException e
) {
320 throw new RuntimeException("Failed read from recipient store", e
);
325 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
328 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
330 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
332 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
333 try (final var connection
= database
.getConnection()) {
334 try (final var statement
= connection
.prepareStatement(sql
)) {
335 try (var result
= Utils
.executeQueryForStream(statement
,
336 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
337 getContactFromResultSet(resultSet
)))) {
338 return result
.toList();
341 } catch (SQLException e
) {
342 throw new RuntimeException("Failed read from recipient store", e
);
346 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
350 r.number, r.aci, r.pni, r.username,
351 r.profile_key, r.profile_key_credential,
352 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,
353 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,
358 ).formatted(TABLE_RECIPIENT
);
359 try (final var statement
= connection
.prepareStatement(sql
)) {
360 statement
.setLong(1, recipientId
.id());
361 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
365 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
369 r.number, r.aci, r.pni, r.username,
370 r.profile_key, r.profile_key_credential,
371 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,
372 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,
375 WHERE r.storage_id = ?
377 ).formatted(TABLE_RECIPIENT
);
378 try (final var statement
= connection
.prepareStatement(sql
)) {
379 statement
.setBytes(1, storageId
.getRaw());
380 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
384 public List
<Recipient
> getRecipients(
385 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
387 final var sqlWhere
= new ArrayList
<String
>();
389 sqlWhere
.add("r.unregistered_timestamp IS NULL");
390 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
391 sqlWhere
.add("r.hidden = FALSE");
393 if (blocked
.isPresent()) {
394 sqlWhere
.add("r.blocked = ?");
396 if (!recipientIds
.isEmpty()) {
397 final var recipientIdsCommaSeparated
= recipientIds
.stream()
398 .map(recipientId
-> String
.valueOf(recipientId
.id()))
399 .collect(Collectors
.joining(","));
400 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
405 r.number, r.aci, r.pni, r.username,
406 r.profile_key, r.profile_key_credential,
407 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,
408 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,
411 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
413 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
414 final var selfAddress
= selfAddressProvider
.getSelfAddress();
415 try (final var connection
= database
.getConnection()) {
416 try (final var statement
= connection
.prepareStatement(sql
)) {
417 if (blocked
.isPresent()) {
418 statement
.setBoolean(1, blocked
.get());
420 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
421 return result
.filter(r
-> name
.isEmpty() || (
422 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
423 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
424 if (r
.getAddress().matches(selfAddress
)) {
425 return Recipient
.newBuilder(r
)
426 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
433 } catch (SQLException e
) {
434 throw new RuntimeException("Failed read from recipient store", e
);
438 public Set
<String
> getAllNumbers() {
443 WHERE r.number IS NOT NULL
445 ).formatted(TABLE_RECIPIENT
);
446 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
447 try (final var connection
= database
.getConnection()) {
448 try (final var statement
= connection
.prepareStatement(sql
)) {
449 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
450 .filter(Objects
::nonNull
)
451 .filter(n
-> !n
.equals(selfNumber
))
456 } catch (NumberFormatException e
) {
460 .collect(Collectors
.toSet());
462 } catch (SQLException e
) {
463 throw new RuntimeException("Failed read from recipient store", e
);
467 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
470 SELECT r.aci, r.profile_key
472 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
474 ).formatted(TABLE_RECIPIENT
);
475 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
476 try (final var connection
= database
.getConnection()) {
477 try (final var statement
= connection
.prepareStatement(sql
)) {
478 return Utils
.executeQueryForStream(statement
, resultSet
-> {
479 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
480 if (aci
.equals(selfAci
)) {
481 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
483 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
484 return new Pair
<>(aci
, profileKey
);
485 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
487 } catch (SQLException e
) {
488 throw new RuntimeException("Failed read from recipient store", e
);
492 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
497 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
499 ).formatted(TABLE_RECIPIENT
);
500 try (final var statement
= connection
.prepareStatement(sql
)) {
501 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
505 public void setMissingStorageIds() {
506 final var selectSql
= (
510 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
512 ).formatted(TABLE_RECIPIENT
);
513 final var updateSql
= (
519 ).formatted(TABLE_RECIPIENT
);
520 try (final var connection
= database
.getConnection()) {
521 connection
.setAutoCommit(false);
522 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
523 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
525 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
526 for (final var recipientId
: recipientIds
) {
527 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
528 updateStmt
.setLong(2, recipientId
.id());
529 updateStmt
.executeUpdate();
534 } catch (SQLException e
) {
535 throw new RuntimeException("Failed update recipient store", e
);
540 public void deleteContact(RecipientId recipientId
) {
541 storeContact(recipientId
, null);
544 public void deleteRecipientData(RecipientId recipientId
) {
545 logger
.debug("Deleting recipient data for {}", recipientId
);
546 try (final var connection
= database
.getConnection()) {
547 connection
.setAutoCommit(false);
548 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
549 storeContact(connection
, recipientId
, null);
550 storeProfile(connection
, recipientId
, null);
551 storeProfileKey(connection
, recipientId
, null, false);
552 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
553 deleteRecipient(connection
, recipientId
);
555 } catch (SQLException e
) {
556 throw new RuntimeException("Failed update recipient store", e
);
561 public Profile
getProfile(final RecipientId recipientId
) {
562 try (final var connection
= database
.getConnection()) {
563 return getProfile(connection
, recipientId
);
564 } catch (SQLException e
) {
565 throw new RuntimeException("Failed read from recipient store", e
);
570 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
571 try (final var connection
= database
.getConnection()) {
572 return getProfileKey(connection
, recipientId
);
573 } catch (SQLException e
) {
574 throw new RuntimeException("Failed read from recipient store", e
);
579 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
580 try (final var connection
= database
.getConnection()) {
581 return getExpiringProfileKeyCredential(connection
, recipientId
);
582 } catch (SQLException e
) {
583 throw new RuntimeException("Failed read from recipient store", e
);
588 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
589 try (final var connection
= database
.getConnection()) {
590 storeProfile(connection
, recipientId
, profile
);
591 } catch (SQLException e
) {
592 throw new RuntimeException("Failed update recipient store", e
);
597 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
598 try (final var connection
= database
.getConnection()) {
599 storeProfileKey(connection
, recipientId
, profileKey
);
600 } catch (SQLException e
) {
601 throw new RuntimeException("Failed update recipient store", e
);
605 public void storeProfileKey(
606 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
607 ) throws SQLException
{
608 storeProfileKey(connection
, recipientId
, profileKey
, true);
612 public void storeExpiringProfileKeyCredential(
613 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
615 try (final var connection
= database
.getConnection()) {
616 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
617 } catch (SQLException e
) {
618 throw new RuntimeException("Failed update recipient store", e
);
622 public void rotateSelfStorageId() {
623 try (final var connection
= database
.getConnection()) {
624 rotateSelfStorageId(connection
);
625 } catch (SQLException e
) {
626 throw new RuntimeException("Failed update recipient store", e
);
630 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
631 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
632 rotateStorageId(connection
, selfRecipientId
);
635 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
636 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
637 return rotateStorageId(connection
, selfRecipientId
);
640 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
643 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)
644 """.formatted(TABLE_RECIPIENT
);
645 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
646 try (final var statement
= connection
.prepareStatement(sql
)) {
647 statement
.setLong(1, selfRecipientId
.id());
648 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
652 public void updateStorageId(
653 Connection connection
, RecipientId recipientId
, StorageId storageId
654 ) throws SQLException
{
661 ).formatted(TABLE_RECIPIENT
);
662 try (final var statement
= connection
.prepareStatement(sql
)) {
663 statement
.setBytes(1, storageId
.getRaw());
664 statement
.setLong(2, recipientId
.id());
665 statement
.executeUpdate();
669 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
676 ).formatted(TABLE_RECIPIENT
);
677 try (final var statement
= connection
.prepareStatement(sql
)) {
678 for (final var entry
: storageIdMap
.entrySet()) {
679 statement
.setBytes(1, entry
.getValue().getRaw());
680 statement
.setLong(2, entry
.getKey().id());
681 statement
.executeUpdate();
686 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
687 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
688 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
691 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
694 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
695 """.formatted(TABLE_RECIPIENT
);
696 try (final var statement
= connection
.prepareStatement(sql
)) {
697 statement
.setLong(1, recipientId
.id());
698 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
699 if (storageId
.isPresent()) {
700 return storageId
.get();
703 return rotateStorageId(connection
, recipientId
);
706 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
707 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
708 updateStorageId(connection
, recipientId
, newStorageId
);
712 public void storeStorageRecord(
713 final Connection connection
,
714 final RecipientId recipientId
,
715 final StorageId storageId
,
716 final byte[] storageRecord
717 ) throws SQLException
{
718 final var deleteSql
= (
721 SET storage_id = NULL
724 ).formatted(TABLE_RECIPIENT
);
725 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
726 statement
.setBytes(1, storageId
.getRaw());
727 statement
.executeUpdate();
729 final var insertSql
= (
732 SET storage_id = ?, storage_record = ?
735 ).formatted(TABLE_RECIPIENT
);
736 try (final var statement
= connection
.prepareStatement(insertSql
)) {
737 statement
.setBytes(1, storageId
.getRaw());
738 if (storageRecord
== null) {
739 statement
.setNull(2, Types
.BLOB
);
741 statement
.setBytes(2, storageRecord
);
743 statement
.setLong(3, recipientId
.id());
744 statement
.executeUpdate();
748 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
749 logger
.debug("Migrating legacy recipients to database");
750 long start
= System
.nanoTime();
753 INSERT INTO %s (_id, number, aci)
756 ).formatted(TABLE_RECIPIENT
);
757 try (final var connection
= database
.getConnection()) {
758 connection
.setAutoCommit(false);
759 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
760 statement
.executeUpdate();
762 try (final var statement
= connection
.prepareStatement(sql
)) {
763 for (final var recipient
: recipients
.values()) {
764 statement
.setLong(1, recipient
.getRecipientId().id());
765 statement
.setString(2, recipient
.getAddress().number().orElse(null));
766 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
767 statement
.executeUpdate();
770 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
772 for (final var recipient
: recipients
.values()) {
773 if (recipient
.getContact() != null) {
774 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
776 if (recipient
.getProfile() != null) {
777 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
779 if (recipient
.getProfileKey() != null) {
780 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
782 if (recipient
.getExpiringProfileKeyCredential() != null) {
783 storeExpiringProfileKeyCredential(connection
,
784 recipient
.getRecipientId(),
785 recipient
.getExpiringProfileKeyCredential());
789 } catch (SQLException e
) {
790 throw new RuntimeException("Failed update recipient store", e
);
792 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
795 long getActualRecipientId(long recipientId
) {
796 while (recipientsMerged
.containsKey(recipientId
)) {
797 final var newRecipientId
= recipientsMerged
.get(recipientId
);
798 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
799 recipientId
= newRecipientId
;
804 public void storeContact(
805 final Connection connection
, final RecipientId recipientId
, final Contact contact
806 ) throws SQLException
{
810 SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?
813 ).formatted(TABLE_RECIPIENT
);
814 try (final var statement
= connection
.prepareStatement(sql
)) {
815 statement
.setString(1, contact
== null ?
null : contact
.givenName());
816 statement
.setString(2, contact
== null ?
null : contact
.familyName());
817 statement
.setString(3, contact
== null ?
null : contact
.nickName());
818 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
819 statement
.setLong(5, contact
== null ?
0 : contact
.muteUntil());
820 statement
.setBoolean(6, contact
!= null && contact
.hideStory());
821 statement
.setBoolean(7, contact
!= null && contact
.isProfileSharingEnabled());
822 statement
.setString(8, contact
== null ?
null : contact
.color());
823 statement
.setBoolean(9, contact
!= null && contact
.isBlocked());
824 statement
.setBoolean(10, contact
!= null && contact
.isArchived());
825 if (contact
== null || contact
.unregisteredTimestamp() == null) {
826 statement
.setNull(11, Types
.INTEGER
);
828 statement
.setLong(11, contact
.unregisteredTimestamp());
830 statement
.setLong(12, recipientId
.id());
831 statement
.executeUpdate();
833 if (contact
!= null && contact
.unregisteredTimestamp() != null) {
834 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
836 rotateStorageId(connection
, recipientId
);
839 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
840 final Connection connection
, final List
<StorageId
> storageIds
841 ) throws SQLException
{
845 SET storage_id = NULL
846 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
848 ).formatted(TABLE_RECIPIENT
);
850 try (final var statement
= connection
.prepareStatement(sql
)) {
851 for (final var storageId
: storageIds
) {
852 statement
.setBytes(1, storageId
.getRaw());
853 count
+= statement
.executeUpdate();
859 public void markUnregistered(final Set
<String
> unregisteredUsers
) {
860 logger
.debug("Marking {} numbers as unregistered", unregisteredUsers
.size());
861 try (final var connection
= database
.getConnection()) {
862 connection
.setAutoCommit(false);
863 for (final var number
: unregisteredUsers
) {
864 final var recipient
= findByNumber(connection
, number
);
865 if (recipient
.isPresent()) {
866 final var recipientId
= recipient
.get().id();
867 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
871 } catch (SQLException e
) {
872 throw new RuntimeException("Failed update recipient store", e
);
876 private void markUnregisteredAndSplitIfNecessary(
877 final Connection connection
, final RecipientId recipientId
878 ) throws SQLException
{
879 markUnregistered(connection
, recipientId
);
880 final var address
= resolveRecipientAddress(connection
, recipientId
);
881 if (address
.aci().isPresent() && address
.pni().isPresent()) {
882 final var numberAddress
= new RecipientAddress(address
.pni().get(), address
.number().orElse(null));
883 updateRecipientAddress(connection
, recipientId
, address
.removeIdentifiersFrom(numberAddress
));
884 addNewRecipient(connection
, numberAddress
);
888 private void markRegistered(
889 final Connection connection
, final RecipientId recipientId
890 ) throws SQLException
{
894 SET unregistered_timestamp = ?
897 ).formatted(TABLE_RECIPIENT
);
898 try (final var statement
= connection
.prepareStatement(sql
)) {
899 statement
.setNull(1, Types
.INTEGER
);
900 statement
.setLong(2, recipientId
.id());
901 statement
.executeUpdate();
905 private void markUnregistered(
906 final Connection connection
, final RecipientId recipientId
907 ) throws SQLException
{
911 SET unregistered_timestamp = ?
912 WHERE _id = ? AND unregistered_timestamp IS NULL
914 ).formatted(TABLE_RECIPIENT
);
915 try (final var statement
= connection
.prepareStatement(sql
)) {
916 statement
.setLong(1, System
.currentTimeMillis());
917 statement
.setLong(2, recipientId
.id());
918 statement
.executeUpdate();
922 private void storeExpiringProfileKeyCredential(
923 final Connection connection
,
924 final RecipientId recipientId
,
925 final ExpiringProfileKeyCredential profileKeyCredential
926 ) throws SQLException
{
930 SET profile_key_credential = ?
933 ).formatted(TABLE_RECIPIENT
);
934 try (final var statement
= connection
.prepareStatement(sql
)) {
935 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
936 statement
.setLong(2, recipientId
.id());
937 statement
.executeUpdate();
941 public void storeProfile(
942 final Connection connection
, final RecipientId recipientId
, final Profile profile
943 ) throws SQLException
{
947 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 = ?
950 ).formatted(TABLE_RECIPIENT
);
951 try (final var statement
= connection
.prepareStatement(sql
)) {
952 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
953 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
954 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
955 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
956 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
957 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
958 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
959 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
960 statement
.setString(9,
963 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
964 statement
.setLong(10, recipientId
.id());
965 statement
.executeUpdate();
967 rotateStorageId(connection
, recipientId
);
970 private void storeProfileKey(
971 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
972 ) throws SQLException
{
973 if (profileKey
!= null) {
974 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
975 if (profileKey
.equals(recipientProfileKey
)) {
976 final var recipientProfile
= getProfile(connection
, recipientId
);
977 if (recipientProfile
== null || (
978 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
979 && recipientProfile
.getUnidentifiedAccessMode()
980 != Profile
.UnidentifiedAccessMode
.DISABLED
990 SET profile_key = ?, profile_key_credential = NULL%s
993 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
994 try (final var statement
= connection
.prepareStatement(sql
)) {
995 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
996 statement
.setLong(2, recipientId
.id());
997 statement
.executeUpdate();
999 rotateStorageId(connection
, recipientId
);
1002 private RecipientAddress
resolveRecipientAddress(
1003 final Connection connection
, final RecipientId recipientId
1004 ) throws SQLException
{
1007 SELECT r.number, r.aci, r.pni, r.username
1011 ).formatted(TABLE_RECIPIENT
);
1012 try (final var statement
= connection
.prepareStatement(sql
)) {
1013 statement
.setLong(1, recipientId
.id());
1014 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
1018 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1019 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1020 try (final var connection
= database
.getConnection()) {
1021 connection
.setAutoCommit(false);
1022 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1023 connection
.commit();
1024 } catch (SQLException e
) {
1025 throw new RuntimeException("Failed update recipient store", e
);
1028 if (!pair
.second().isEmpty()) {
1029 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1030 try (final var connection
= database
.getConnection()) {
1031 connection
.setAutoCommit(false);
1032 mergeRecipients(connection
, pair
.first(), pair
.second());
1033 connection
.commit();
1034 } catch (SQLException e
) {
1035 throw new RuntimeException("Failed update recipient store", e
);
1038 return pair
.first();
1041 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1042 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1043 ) throws SQLException
{
1044 if (address
.hasSingleIdentifier() || (
1045 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1047 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1049 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1050 markRegistered(connection
, pair
.first());
1052 for (final var toBeMergedRecipientId
: pair
.second()) {
1053 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1059 private void mergeRecipients(
1060 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1061 ) throws SQLException
{
1062 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1063 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1064 deleteRecipient(connection
, toBeMergedRecipientId
);
1065 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1069 private RecipientId
resolveRecipientLocked(
1070 Connection connection
, RecipientAddress address
1071 ) throws SQLException
{
1072 final var byAci
= address
.aci().isEmpty()
1073 ? Optional
.<RecipientWithAddress
>empty()
1074 : findByServiceId(connection
, address
.aci().get());
1076 if (byAci
.isPresent()) {
1077 return byAci
.get().id();
1080 final var byPni
= address
.pni().isEmpty()
1081 ? Optional
.<RecipientWithAddress
>empty()
1082 : findByServiceId(connection
, address
.pni().get());
1084 if (byPni
.isPresent()) {
1085 return byPni
.get().id();
1088 final var byNumber
= address
.number().isEmpty()
1089 ? Optional
.<RecipientWithAddress
>empty()
1090 : findByNumber(connection
, address
.number().get());
1092 if (byNumber
.isPresent()) {
1093 return byNumber
.get().id();
1096 logger
.debug("Got new recipient, both serviceId and number are unknown");
1098 if (address
.serviceId().isEmpty()) {
1099 return addNewRecipient(connection
, address
);
1102 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1105 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1106 final var recipient
= findByServiceId(connection
, serviceId
);
1108 if (recipient
.isEmpty()) {
1109 logger
.debug("Got new recipient, serviceId is unknown");
1110 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1113 return recipient
.get().id();
1116 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1117 final var recipient
= findByNumber(connection
, number
);
1119 if (recipient
.isEmpty()) {
1120 logger
.debug("Got new recipient, number is unknown");
1121 return addNewRecipient(connection
, new RecipientAddress(number
));
1124 return recipient
.get().id();
1127 private RecipientId
addNewRecipient(
1128 final Connection connection
, final RecipientAddress address
1129 ) throws SQLException
{
1132 INSERT INTO %s (number, aci, pni, username)
1136 ).formatted(TABLE_RECIPIENT
);
1137 try (final var statement
= connection
.prepareStatement(sql
)) {
1138 statement
.setString(1, address
.number().orElse(null));
1139 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1140 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1141 statement
.setString(4, address
.username().orElse(null));
1142 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1143 if (generatedKey
.isPresent()) {
1144 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1145 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1148 throw new RuntimeException("Failed to add new recipient to database");
1153 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1154 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1158 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1161 ).formatted(TABLE_RECIPIENT
);
1162 try (final var statement
= connection
.prepareStatement(sql
)) {
1163 statement
.setLong(1, recipientId
.id());
1164 statement
.executeUpdate();
1168 private void updateRecipientAddress(
1169 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1170 ) throws SQLException
{
1171 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1175 SET number = ?, aci = ?, pni = ?, username = ?
1178 ).formatted(TABLE_RECIPIENT
);
1179 try (final var statement
= connection
.prepareStatement(sql
)) {
1180 statement
.setString(1, address
.number().orElse(null));
1181 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1182 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1183 statement
.setString(4, address
.username().orElse(null));
1184 statement
.setLong(5, recipientId
.id());
1185 statement
.executeUpdate();
1187 rotateStorageId(connection
, recipientId
);
1190 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1196 ).formatted(TABLE_RECIPIENT
);
1197 try (final var statement
= connection
.prepareStatement(sql
)) {
1198 statement
.setLong(1, recipientId
.id());
1199 statement
.executeUpdate();
1203 private void mergeRecipientsLocked(
1204 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1205 ) throws SQLException
{
1206 final var contact
= getContact(connection
, recipientId
);
1207 if (contact
== null) {
1208 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1209 storeContact(connection
, recipientId
, toBeMergedContact
);
1212 final var profileKey
= getProfileKey(connection
, recipientId
);
1213 if (profileKey
== null) {
1214 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1215 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1218 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1219 if (profileKeyCredential
== null) {
1220 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1221 toBeMergedRecipientId
);
1222 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1225 final var profile
= getProfile(connection
, recipientId
);
1226 if (profile
== null) {
1227 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1228 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1231 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1234 private Optional
<RecipientWithAddress
> findByNumber(
1235 final Connection connection
, final String number
1236 ) throws SQLException
{
1238 SELECT r._id, r.number, r.aci, r.pni, r.username
1242 """.formatted(TABLE_RECIPIENT
);
1243 try (final var statement
= connection
.prepareStatement(sql
)) {
1244 statement
.setString(1, number
);
1245 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1249 private Optional
<RecipientWithAddress
> findByUsername(
1250 final Connection connection
, final String username
1251 ) throws SQLException
{
1253 SELECT r._id, r.number, r.aci, r.pni, r.username
1255 WHERE r.username = ?
1257 """.formatted(TABLE_RECIPIENT
);
1258 try (final var statement
= connection
.prepareStatement(sql
)) {
1259 statement
.setString(1, username
);
1260 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1264 private Optional
<RecipientWithAddress
> findByServiceId(
1265 final Connection connection
, final ServiceId serviceId
1266 ) throws SQLException
{
1267 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1268 if (recipientWithAddress
.isPresent()) {
1269 return recipientWithAddress
;
1272 SELECT r._id, r.number, r.aci, r.pni, r.username
1276 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1277 try (final var statement
= connection
.prepareStatement(sql
)) {
1278 statement
.setString(1, serviceId
.toString());
1279 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1280 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1281 return recipientWithAddress
;
1285 private Set
<RecipientWithAddress
> findAllByAddress(
1286 final Connection connection
, final RecipientAddress address
1287 ) throws SQLException
{
1289 SELECT r._id, r.number, r.aci, r.pni, r.username
1295 """.formatted(TABLE_RECIPIENT
);
1296 try (final var statement
= connection
.prepareStatement(sql
)) {
1297 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1298 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1299 statement
.setString(3, address
.number().orElse(null));
1300 statement
.setString(4, address
.username().orElse(null));
1301 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1302 .collect(Collectors
.toSet());
1306 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1309 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
1311 WHERE r._id = ? AND (%s)
1313 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1314 try (final var statement
= connection
.prepareStatement(sql
)) {
1315 statement
.setLong(1, recipientId
.id());
1316 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1320 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1321 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1322 if (recipientId
.equals(selfRecipientId
)) {
1323 return selfProfileKeyProvider
.getSelfProfileKey();
1327 SELECT r.profile_key
1331 ).formatted(TABLE_RECIPIENT
);
1332 try (final var statement
= connection
.prepareStatement(sql
)) {
1333 statement
.setLong(1, recipientId
.id());
1334 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1338 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1339 final Connection connection
, final RecipientId recipientId
1340 ) throws SQLException
{
1343 SELECT r.profile_key_credential
1347 ).formatted(TABLE_RECIPIENT
);
1348 try (final var statement
= connection
.prepareStatement(sql
)) {
1349 statement
.setLong(1, recipientId
.id());
1350 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1355 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1358 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
1360 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1362 ).formatted(TABLE_RECIPIENT
);
1363 try (final var statement
= connection
.prepareStatement(sql
)) {
1364 statement
.setLong(1, recipientId
.id());
1365 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1369 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1370 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrThrow
);
1371 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrThrow
);
1372 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1373 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1374 return new RecipientAddress(aci
, pni
, number
, username
);
1377 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1378 return new RecipientId(resultSet
.getLong("_id"), this);
1381 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1382 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1383 getRecipientAddressFromResultSet(resultSet
));
1386 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1387 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1388 getRecipientAddressFromResultSet(resultSet
),
1389 getContactFromResultSet(resultSet
),
1390 getProfileKeyFromResultSet(resultSet
),
1391 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1392 getProfileFromResultSet(resultSet
),
1393 getStorageRecordFromResultSet(resultSet
));
1396 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1397 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1398 return new Contact(resultSet
.getString("given_name"),
1399 resultSet
.getString("family_name"),
1400 resultSet
.getString("nick_name"),
1401 resultSet
.getString("color"),
1402 resultSet
.getInt("expiration_time"),
1403 resultSet
.getLong("mute_until"),
1404 resultSet
.getBoolean("hide_story"),
1405 resultSet
.getBoolean("blocked"),
1406 resultSet
.getBoolean("archived"),
1407 resultSet
.getBoolean("profile_sharing"),
1408 resultSet
.getBoolean("hidden"),
1409 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1412 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1413 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1414 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1415 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1416 resultSet
.getString("profile_given_name"),
1417 resultSet
.getString("profile_family_name"),
1418 resultSet
.getString("profile_about"),
1419 resultSet
.getString("profile_about_emoji"),
1420 resultSet
.getString("profile_avatar_url_path"),
1421 resultSet
.getBytes("profile_mobile_coin_address"),
1422 profileUnidentifiedAccessMode
== null
1423 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1424 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1425 profileCapabilities
== null
1427 : Arrays
.stream(profileCapabilities
.split(","))
1428 .map(Profile
.Capability
::valueOfOrNull
)
1429 .filter(Objects
::nonNull
)
1430 .collect(Collectors
.toSet()));
1433 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1434 final var profileKey
= resultSet
.getBytes("profile_key");
1436 if (profileKey
== null) {
1440 return new ProfileKey(profileKey
);
1441 } catch (InvalidInputException ignored
) {
1446 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1447 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1449 if (profileKeyCredential
== null) {
1453 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1454 } catch (Throwable ignored
) {
1459 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1460 final var storageId
= resultSet
.getBytes("storage_id");
1461 return StorageId
.forContact(storageId
);
1464 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1465 return resultSet
.getBytes("storage_record");
1468 public interface RecipientMergeHandler
{
1470 void mergeRecipients(
1471 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1472 ) throws SQLException
;
1475 private class HelperStore
implements MergeRecipientHelper
.Store
{
1477 private final Connection connection
;
1479 public HelperStore(final Connection connection
) {
1480 this.connection
= connection
;
1484 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1485 return RecipientStore
.this.findAllByAddress(connection
, address
);
1489 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1490 return RecipientStore
.this.addNewRecipient(connection
, address
);
1494 public void updateRecipientAddress(
1495 final RecipientId recipientId
, final RecipientAddress address
1496 ) throws SQLException
{
1497 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1501 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1502 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);