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
.PhoneNumberSharingMode
;
6 import org
.asamk
.signal
.manager
.api
.Profile
;
7 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
8 import org
.asamk
.signal
.manager
.storage
.Database
;
9 import org
.asamk
.signal
.manager
.storage
.Utils
;
10 import org
.asamk
.signal
.manager
.storage
.contacts
.ContactsStore
;
11 import org
.asamk
.signal
.manager
.storage
.profiles
.ProfileStore
;
12 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
13 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
14 import org
.signal
.libsignal
.zkgroup
.profiles
.ExpiringProfileKeyCredential
;
15 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
16 import org
.slf4j
.Logger
;
17 import org
.slf4j
.LoggerFactory
;
18 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
19 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
20 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
21 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
22 import org
.whispersystems
.signalservice
.api
.storage
.StorageId
;
24 import java
.sql
.Connection
;
25 import java
.sql
.ResultSet
;
26 import java
.sql
.SQLException
;
27 import java
.sql
.Types
;
28 import java
.util
.ArrayList
;
29 import java
.util
.Arrays
;
30 import java
.util
.Collection
;
31 import java
.util
.HashMap
;
32 import java
.util
.List
;
34 import java
.util
.Objects
;
35 import java
.util
.Optional
;
37 import java
.util
.function
.Supplier
;
38 import java
.util
.stream
.Collectors
;
40 public class RecipientStore
implements RecipientIdCreator
, RecipientResolver
, RecipientTrustedResolver
, ContactsStore
, ProfileStore
{
42 private static final Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
43 private static final String TABLE_RECIPIENT
= "recipient";
44 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.nick_name_given_name IS NOT NULL OR r.nick_name_family_name IS NOT NULL OR r.note 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";
46 private final RecipientMergeHandler recipientMergeHandler
;
47 private final SelfAddressProvider selfAddressProvider
;
48 private final SelfProfileKeyProvider selfProfileKeyProvider
;
49 private final Database database
;
51 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
53 private final Map
<ServiceId
, RecipientWithAddress
> recipientAddressCache
= new HashMap
<>();
55 public static void createSql(Connection connection
) throws SQLException
{
56 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
57 try (final var statement
= connection
.createStatement()) {
58 statement
.executeUpdate("""
59 CREATE TABLE recipient (
60 _id INTEGER PRIMARY KEY AUTOINCREMENT,
61 storage_id BLOB UNIQUE,
67 unregistered_timestamp INTEGER,
70 profile_key_credential BLOB,
71 needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
76 nick_name_given_name TEXT,
77 nick_name_family_name TEXT,
81 expiration_time INTEGER NOT NULL DEFAULT 0,
82 mute_until INTEGER NOT NULL DEFAULT 0,
83 blocked INTEGER NOT NULL DEFAULT FALSE,
84 archived INTEGER NOT NULL DEFAULT FALSE,
85 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
86 hide_story INTEGER NOT NULL DEFAULT FALSE,
87 hidden INTEGER NOT NULL DEFAULT FALSE,
89 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
90 profile_given_name TEXT,
91 profile_family_name TEXT,
93 profile_about_emoji TEXT,
94 profile_avatar_url_path TEXT,
95 profile_mobile_coin_address BLOB,
96 profile_unidentified_access_mode TEXT,
97 profile_capabilities TEXT,
98 profile_phone_number_sharing TEXT
104 public RecipientStore(
105 final RecipientMergeHandler recipientMergeHandler
,
106 final SelfAddressProvider selfAddressProvider
,
107 final SelfProfileKeyProvider selfProfileKeyProvider
,
108 final Database database
110 this.recipientMergeHandler
= recipientMergeHandler
;
111 this.selfAddressProvider
= selfAddressProvider
;
112 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
113 this.database
= database
;
116 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
117 try (final var connection
= database
.getConnection()) {
118 return resolveRecipientAddress(connection
, recipientId
);
119 } catch (SQLException e
) {
120 throw new RuntimeException("Failed read from recipient store", e
);
124 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
129 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
131 ).formatted(TABLE_RECIPIENT
);
132 try (final var connection
= database
.getConnection()) {
133 try (final var statement
= connection
.prepareStatement(sql
)) {
134 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
135 return result
.toList();
138 } catch (SQLException e
) {
139 throw new RuntimeException("Failed read from recipient store", e
);
144 public RecipientId
resolveRecipient(final long rawRecipientId
) {
151 ).formatted(TABLE_RECIPIENT
);
152 try (final var connection
= database
.getConnection()) {
153 try (final var statement
= connection
.prepareStatement(sql
)) {
154 statement
.setLong(1, rawRecipientId
);
155 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
157 } catch (SQLException e
) {
158 throw new RuntimeException("Failed read from recipient store", e
);
163 public RecipientId
resolveRecipient(final String identifier
) {
164 final var serviceId
= ServiceId
.parseOrNull(identifier
);
165 if (serviceId
!= null) {
166 return resolveRecipient(serviceId
);
168 return resolveRecipientByNumber(identifier
);
172 private RecipientId
resolveRecipientByNumber(final String number
) {
173 final RecipientId recipientId
;
174 try (final var connection
= database
.getConnection()) {
175 connection
.setAutoCommit(false);
176 recipientId
= resolveRecipientLocked(connection
, number
);
178 } catch (SQLException e
) {
179 throw new RuntimeException("Failed read recipient store", e
);
185 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
186 try (final var connection
= database
.getConnection()) {
187 connection
.setAutoCommit(false);
188 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
189 if (recipientWithAddress
!= null) {
190 return recipientWithAddress
.id();
192 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
195 } catch (SQLException e
) {
196 throw new RuntimeException("Failed read recipient store", e
);
201 * Should only be used for recipientIds from the database.
202 * Where the foreign key relations ensure a valid recipientId.
205 public RecipientId
create(final long recipientId
) {
206 return new RecipientId(recipientId
, this);
209 public RecipientId
resolveRecipientByNumber(
210 final String number
, Supplier
<ServiceId
> serviceIdSupplier
211 ) throws UnregisteredRecipientException
{
212 final Optional
<RecipientWithAddress
> byNumber
;
213 try (final var connection
= database
.getConnection()) {
214 byNumber
= findByNumber(connection
, number
);
215 } catch (SQLException e
) {
216 throw new RuntimeException("Failed read from recipient store", e
);
218 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
219 final var serviceId
= serviceIdSupplier
.get();
220 if (serviceId
== null) {
221 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(number
));
224 return resolveRecipient(serviceId
);
226 return byNumber
.get().id();
229 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
230 final Optional
<RecipientWithAddress
> byNumber
;
231 try (final var connection
= database
.getConnection()) {
232 byNumber
= findByNumber(connection
, number
);
233 } catch (SQLException e
) {
234 throw new RuntimeException("Failed read from recipient store", e
);
236 return byNumber
.map(RecipientWithAddress
::id
);
239 public RecipientId
resolveRecipientByUsername(
240 final String username
, Supplier
<ACI
> aciSupplier
241 ) throws UnregisteredRecipientException
{
242 final Optional
<RecipientWithAddress
> byUsername
;
243 try (final var connection
= database
.getConnection()) {
244 byUsername
= findByUsername(connection
, username
);
245 } catch (SQLException e
) {
246 throw new RuntimeException("Failed read from recipient store", e
);
248 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
249 final var aci
= aciSupplier
.get();
251 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
257 return resolveRecipientTrusted(aci
, username
);
259 return byUsername
.get().id();
262 public RecipientId
resolveRecipient(RecipientAddress address
) {
263 final RecipientId recipientId
;
264 try (final var connection
= database
.getConnection()) {
265 connection
.setAutoCommit(false);
266 recipientId
= resolveRecipientLocked(connection
, address
);
268 } catch (SQLException e
) {
269 throw new RuntimeException("Failed read recipient store", e
);
274 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
275 return resolveRecipientLocked(connection
, address
);
279 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
280 return resolveRecipientTrusted(address
, true);
284 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
285 return resolveRecipientTrusted(address
, false);
288 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
289 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
290 if (!pair
.second().isEmpty()) {
291 mergeRecipients(connection
, pair
.first(), pair
.second());
297 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
298 return resolveRecipientTrusted(new RecipientAddress(address
));
302 public RecipientId
resolveRecipientTrusted(
303 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
305 return resolveRecipientTrusted(new RecipientAddress(aci
, pni
, number
, Optional
.empty()));
309 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
310 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
314 public void storeContact(RecipientId recipientId
, final Contact contact
) {
315 try (final var connection
= database
.getConnection()) {
316 storeContact(connection
, recipientId
, contact
);
317 } catch (SQLException e
) {
318 throw new RuntimeException("Failed update recipient store", e
);
323 public Contact
getContact(RecipientId recipientId
) {
324 try (final var connection
= database
.getConnection()) {
325 return getContact(connection
, recipientId
);
326 } catch (SQLException e
) {
327 throw new RuntimeException("Failed read from recipient store", e
);
332 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
335 SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
337 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
339 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
340 try (final var connection
= database
.getConnection()) {
341 try (final var statement
= connection
.prepareStatement(sql
)) {
342 try (var result
= Utils
.executeQueryForStream(statement
,
343 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
344 getContactFromResultSet(resultSet
)))) {
345 return result
.toList();
348 } catch (SQLException e
) {
349 throw new RuntimeException("Failed read from recipient store", e
);
353 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
357 r.number, r.aci, r.pni, r.username,
358 r.profile_key, r.profile_key_credential,
359 r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
360 r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
366 ).formatted(TABLE_RECIPIENT
);
367 try (final var statement
= connection
.prepareStatement(sql
)) {
368 statement
.setLong(1, recipientId
.id());
369 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
373 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
377 r.number, r.aci, r.pni, r.username,
378 r.profile_key, r.profile_key_credential,
379 r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
380 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, r.profile_phone_number_sharing,
384 WHERE r.storage_id = ?
386 ).formatted(TABLE_RECIPIENT
);
387 try (final var statement
= connection
.prepareStatement(sql
)) {
388 statement
.setBytes(1, storageId
.getRaw());
389 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
393 public List
<Recipient
> getRecipients(
394 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
396 final var sqlWhere
= new ArrayList
<String
>();
398 sqlWhere
.add("r.unregistered_timestamp IS NULL");
399 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
400 sqlWhere
.add("r.hidden = FALSE");
402 if (blocked
.isPresent()) {
403 sqlWhere
.add("r.blocked = ?");
405 if (!recipientIds
.isEmpty()) {
406 final var recipientIdsCommaSeparated
= recipientIds
.stream()
407 .map(recipientId
-> String
.valueOf(recipientId
.id()))
408 .collect(Collectors
.joining(","));
409 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
414 r.number, r.aci, r.pni, r.username,
415 r.profile_key, r.profile_key_credential,
416 r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
417 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, r.profile_phone_number_sharing,
421 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
423 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
424 final var selfAddress
= selfAddressProvider
.getSelfAddress();
425 try (final var connection
= database
.getConnection()) {
426 try (final var statement
= connection
.prepareStatement(sql
)) {
427 if (blocked
.isPresent()) {
428 statement
.setBoolean(1, blocked
.get());
430 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
431 return result
.filter(r
-> name
.isEmpty() || (
432 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
433 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
434 if (r
.getAddress().matches(selfAddress
)) {
435 return Recipient
.newBuilder(r
)
436 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
443 } catch (SQLException e
) {
444 throw new RuntimeException("Failed read from recipient store", e
);
448 public Set
<String
> getAllNumbers() {
453 WHERE r.number IS NOT NULL
455 ).formatted(TABLE_RECIPIENT
);
456 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
457 try (final var connection
= database
.getConnection()) {
458 try (final var statement
= connection
.prepareStatement(sql
)) {
459 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
460 .filter(Objects
::nonNull
)
461 .filter(n
-> !n
.equals(selfNumber
))
466 } catch (NumberFormatException e
) {
470 .collect(Collectors
.toSet());
472 } catch (SQLException e
) {
473 throw new RuntimeException("Failed read from recipient store", e
);
477 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
480 SELECT r.aci, r.profile_key
482 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
484 ).formatted(TABLE_RECIPIENT
);
485 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
486 try (final var connection
= database
.getConnection()) {
487 try (final var statement
= connection
.prepareStatement(sql
)) {
488 return Utils
.executeQueryForStream(statement
, resultSet
-> {
489 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
490 if (aci
.equals(selfAci
)) {
491 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
493 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
494 return new Pair
<>(aci
, profileKey
);
495 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
497 } catch (SQLException e
) {
498 throw new RuntimeException("Failed read from recipient store", e
);
502 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
507 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
509 ).formatted(TABLE_RECIPIENT
);
510 try (final var statement
= connection
.prepareStatement(sql
)) {
511 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
515 public void setMissingStorageIds() {
516 final var selectSql
= (
520 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
522 ).formatted(TABLE_RECIPIENT
);
523 final var updateSql
= (
529 ).formatted(TABLE_RECIPIENT
);
530 try (final var connection
= database
.getConnection()) {
531 connection
.setAutoCommit(false);
532 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
533 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
535 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
536 for (final var recipientId
: recipientIds
) {
537 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
538 updateStmt
.setLong(2, recipientId
.id());
539 updateStmt
.executeUpdate();
544 } catch (SQLException e
) {
545 throw new RuntimeException("Failed update recipient store", e
);
550 public void deleteContact(RecipientId recipientId
) {
551 storeContact(recipientId
, null);
554 public void deleteRecipientData(RecipientId recipientId
) {
555 logger
.debug("Deleting recipient data for {}", recipientId
);
556 try (final var connection
= database
.getConnection()) {
557 connection
.setAutoCommit(false);
558 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
559 storeContact(connection
, recipientId
, null);
560 storeProfile(connection
, recipientId
, null);
561 storeProfileKey(connection
, recipientId
, null, false);
562 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
563 deleteRecipient(connection
, recipientId
);
565 } catch (SQLException e
) {
566 throw new RuntimeException("Failed update recipient store", e
);
571 public Profile
getProfile(final RecipientId recipientId
) {
572 try (final var connection
= database
.getConnection()) {
573 return getProfile(connection
, recipientId
);
574 } catch (SQLException e
) {
575 throw new RuntimeException("Failed read from recipient store", e
);
580 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
581 try (final var connection
= database
.getConnection()) {
582 return getProfileKey(connection
, recipientId
);
583 } catch (SQLException e
) {
584 throw new RuntimeException("Failed read from recipient store", e
);
589 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
590 try (final var connection
= database
.getConnection()) {
591 return getExpiringProfileKeyCredential(connection
, recipientId
);
592 } catch (SQLException e
) {
593 throw new RuntimeException("Failed read from recipient store", e
);
598 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
599 try (final var connection
= database
.getConnection()) {
600 storeProfile(connection
, recipientId
, profile
);
601 } catch (SQLException e
) {
602 throw new RuntimeException("Failed update recipient store", e
);
607 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
608 try (final var connection
= database
.getConnection()) {
609 storeProfileKey(connection
, recipientId
, profileKey
);
610 } catch (SQLException e
) {
611 throw new RuntimeException("Failed update recipient store", e
);
615 public void storeProfileKey(
616 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
617 ) throws SQLException
{
618 storeProfileKey(connection
, recipientId
, profileKey
, true);
622 public void storeExpiringProfileKeyCredential(
623 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
625 try (final var connection
= database
.getConnection()) {
626 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
627 } catch (SQLException e
) {
628 throw new RuntimeException("Failed update recipient store", e
);
632 public void rotateSelfStorageId() {
633 try (final var connection
= database
.getConnection()) {
634 rotateSelfStorageId(connection
);
635 } catch (SQLException e
) {
636 throw new RuntimeException("Failed update recipient store", e
);
640 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
641 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
642 rotateStorageId(connection
, selfRecipientId
);
645 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
646 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
647 return rotateStorageId(connection
, selfRecipientId
);
650 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
653 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)
654 """.formatted(TABLE_RECIPIENT
);
655 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
656 try (final var statement
= connection
.prepareStatement(sql
)) {
657 statement
.setLong(1, selfRecipientId
.id());
658 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
662 public void updateStorageId(
663 Connection connection
, RecipientId recipientId
, StorageId storageId
664 ) throws SQLException
{
671 ).formatted(TABLE_RECIPIENT
);
672 try (final var statement
= connection
.prepareStatement(sql
)) {
673 statement
.setBytes(1, storageId
.getRaw());
674 statement
.setLong(2, recipientId
.id());
675 statement
.executeUpdate();
679 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
686 ).formatted(TABLE_RECIPIENT
);
687 try (final var statement
= connection
.prepareStatement(sql
)) {
688 for (final var entry
: storageIdMap
.entrySet()) {
689 statement
.setBytes(1, entry
.getValue().getRaw());
690 statement
.setLong(2, entry
.getKey().id());
691 statement
.executeUpdate();
696 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
697 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
698 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
701 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
704 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
705 """.formatted(TABLE_RECIPIENT
);
706 try (final var statement
= connection
.prepareStatement(sql
)) {
707 statement
.setLong(1, recipientId
.id());
708 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
709 if (storageId
.isPresent()) {
710 return storageId
.get();
713 return rotateStorageId(connection
, recipientId
);
716 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
717 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
718 updateStorageId(connection
, recipientId
, newStorageId
);
722 public void storeStorageRecord(
723 final Connection connection
,
724 final RecipientId recipientId
,
725 final StorageId storageId
,
726 final byte[] storageRecord
727 ) throws SQLException
{
728 final var deleteSql
= (
731 SET storage_id = NULL
734 ).formatted(TABLE_RECIPIENT
);
735 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
736 statement
.setBytes(1, storageId
.getRaw());
737 statement
.executeUpdate();
739 final var insertSql
= (
742 SET storage_id = ?, storage_record = ?
745 ).formatted(TABLE_RECIPIENT
);
746 try (final var statement
= connection
.prepareStatement(insertSql
)) {
747 statement
.setBytes(1, storageId
.getRaw());
748 if (storageRecord
== null) {
749 statement
.setNull(2, Types
.BLOB
);
751 statement
.setBytes(2, storageRecord
);
753 statement
.setLong(3, recipientId
.id());
754 statement
.executeUpdate();
758 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
759 logger
.debug("Migrating legacy recipients to database");
760 long start
= System
.nanoTime();
763 INSERT INTO %s (_id, number, aci)
766 ).formatted(TABLE_RECIPIENT
);
767 try (final var connection
= database
.getConnection()) {
768 connection
.setAutoCommit(false);
769 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
770 statement
.executeUpdate();
772 try (final var statement
= connection
.prepareStatement(sql
)) {
773 for (final var recipient
: recipients
.values()) {
774 statement
.setLong(1, recipient
.getRecipientId().id());
775 statement
.setString(2, recipient
.getAddress().number().orElse(null));
776 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
777 statement
.executeUpdate();
780 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
782 for (final var recipient
: recipients
.values()) {
783 if (recipient
.getContact() != null) {
784 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
786 if (recipient
.getProfile() != null) {
787 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
789 if (recipient
.getProfileKey() != null) {
790 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
792 if (recipient
.getExpiringProfileKeyCredential() != null) {
793 storeExpiringProfileKeyCredential(connection
,
794 recipient
.getRecipientId(),
795 recipient
.getExpiringProfileKeyCredential());
799 } catch (SQLException e
) {
800 throw new RuntimeException("Failed update recipient store", e
);
802 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
805 long getActualRecipientId(long recipientId
) {
806 while (recipientsMerged
.containsKey(recipientId
)) {
807 final var newRecipientId
= recipientsMerged
.get(recipientId
);
808 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
809 recipientId
= newRecipientId
;
814 public void storeContact(
815 final Connection connection
, final RecipientId recipientId
, final Contact contact
816 ) throws SQLException
{
820 SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?
823 ).formatted(TABLE_RECIPIENT
);
824 try (final var statement
= connection
.prepareStatement(sql
)) {
825 statement
.setString(1, contact
== null ?
null : contact
.givenName());
826 statement
.setString(2, contact
== null ?
null : contact
.familyName());
827 statement
.setString(3, contact
== null ?
null : contact
.nickName());
828 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
829 statement
.setLong(5, contact
== null ?
0 : contact
.muteUntil());
830 statement
.setBoolean(6, contact
!= null && contact
.hideStory());
831 statement
.setBoolean(7, contact
!= null && contact
.isProfileSharingEnabled());
832 statement
.setString(8, contact
== null ?
null : contact
.color());
833 statement
.setBoolean(9, contact
!= null && contact
.isBlocked());
834 statement
.setBoolean(10, contact
!= null && contact
.isArchived());
835 if (contact
== null || contact
.unregisteredTimestamp() == null) {
836 statement
.setNull(11, Types
.INTEGER
);
838 statement
.setLong(11, contact
.unregisteredTimestamp());
840 statement
.setString(12, contact
== null ?
null : contact
.nickNameGivenName());
841 statement
.setString(13, contact
== null ?
null : contact
.nickNameFamilyName());
842 statement
.setString(14, contact
== null ?
null : contact
.note());
843 statement
.setLong(15, recipientId
.id());
844 statement
.executeUpdate();
846 if (contact
!= null && contact
.unregisteredTimestamp() != null) {
847 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
849 rotateStorageId(connection
, recipientId
);
852 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
853 final Connection connection
, final List
<StorageId
> storageIds
854 ) throws SQLException
{
858 SET storage_id = NULL
859 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
861 ).formatted(TABLE_RECIPIENT
);
863 try (final var statement
= connection
.prepareStatement(sql
)) {
864 for (final var storageId
: storageIds
) {
865 statement
.setBytes(1, storageId
.getRaw());
866 count
+= statement
.executeUpdate();
872 public void markNeedsPniSignature(final RecipientId recipientId
, final boolean value
) {
873 logger
.debug("Marking {} numbers as need pni signature = {}", recipientId
, value
);
874 try (final var connection
= database
.getConnection()) {
878 SET needs_pni_signature = ?
881 ).formatted(TABLE_RECIPIENT
);
882 try (final var statement
= connection
.prepareStatement(sql
)) {
883 statement
.setBoolean(1, value
);
884 statement
.setLong(2, recipientId
.id());
885 statement
.executeUpdate();
887 } catch (SQLException e
) {
888 throw new RuntimeException("Failed update recipient store", e
);
892 public boolean needsPniSignature(final RecipientId recipientId
) {
893 try (final var connection
= database
.getConnection()) {
896 SELECT needs_pni_signature
900 ).formatted(TABLE_RECIPIENT
);
901 try (final var statement
= connection
.prepareStatement(sql
)) {
902 statement
.setLong(1, recipientId
.id());
903 return Utils
.executeQuerySingleRow(statement
, resultSet
-> resultSet
.getBoolean("needs_pni_signature"));
905 } catch (SQLException e
) {
906 throw new RuntimeException("Failed read recipient store", e
);
910 public void markUndiscoverablePossiblyUnregistered(final Set
<String
> numbers
) {
911 logger
.debug("Marking {} numbers as unregistered", numbers
.size());
912 try (final var connection
= database
.getConnection()) {
913 connection
.setAutoCommit(false);
914 for (final var number
: numbers
) {
915 final var recipientAddress
= findByNumber(connection
, number
);
916 if (recipientAddress
.isPresent()) {
917 final var recipientId
= recipientAddress
.get().id();
918 markDiscoverable(connection
, recipientId
, false);
919 final var contact
= getContact(connection
, recipientId
);
920 if (recipientAddress
.get().address().aci().isEmpty() || (
922 && contact
.unregisteredTimestamp() != null
924 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
929 } catch (SQLException e
) {
930 throw new RuntimeException("Failed update recipient store", e
);
934 public void markDiscoverable(final Set
<String
> numbers
) {
935 logger
.debug("Marking {} numbers as discoverable", numbers
.size());
936 try (final var connection
= database
.getConnection()) {
937 connection
.setAutoCommit(false);
938 for (final var number
: numbers
) {
939 final var recipientAddress
= findByNumber(connection
, number
);
940 if (recipientAddress
.isPresent()) {
941 final var recipientId
= recipientAddress
.get().id();
942 markDiscoverable(connection
, recipientId
, true);
946 } catch (SQLException e
) {
947 throw new RuntimeException("Failed update recipient store", e
);
951 public void markRegistered(final RecipientId recipientId
, final boolean registered
) {
952 logger
.debug("Marking {} as registered={}", recipientId
, registered
);
953 try (final var connection
= database
.getConnection()) {
954 connection
.setAutoCommit(false);
956 markRegistered(connection
, recipientId
);
958 markUnregistered(connection
, recipientId
);
961 } catch (SQLException e
) {
962 throw new RuntimeException("Failed update recipient store", e
);
966 private void markUnregisteredAndSplitIfNecessary(
967 final Connection connection
, final RecipientId recipientId
968 ) throws SQLException
{
969 markUnregistered(connection
, recipientId
);
970 final var address
= resolveRecipientAddress(connection
, recipientId
);
971 if (address
.aci().isPresent() && address
.pni().isPresent()) {
972 final var numberAddress
= new RecipientAddress(address
.pni().get(), address
.number().orElse(null));
973 updateRecipientAddress(connection
, recipientId
, address
.removeIdentifiersFrom(numberAddress
));
974 addNewRecipient(connection
, numberAddress
);
978 private void markDiscoverable(
979 final Connection connection
, final RecipientId recipientId
, final boolean discoverable
980 ) throws SQLException
{
987 ).formatted(TABLE_RECIPIENT
);
988 try (final var statement
= connection
.prepareStatement(sql
)) {
989 statement
.setBoolean(1, discoverable
);
990 statement
.setLong(2, recipientId
.id());
991 statement
.executeUpdate();
995 private void markRegistered(
996 final Connection connection
, final RecipientId recipientId
997 ) throws SQLException
{
1001 SET unregistered_timestamp = NULL
1004 ).formatted(TABLE_RECIPIENT
);
1005 try (final var statement
= connection
.prepareStatement(sql
)) {
1006 statement
.setLong(1, recipientId
.id());
1007 statement
.executeUpdate();
1011 private void markUnregistered(
1012 final Connection connection
, final RecipientId recipientId
1013 ) throws SQLException
{
1017 SET unregistered_timestamp = ?, discoverable = FALSE
1020 ).formatted(TABLE_RECIPIENT
);
1021 try (final var statement
= connection
.prepareStatement(sql
)) {
1022 statement
.setLong(1, System
.currentTimeMillis());
1023 statement
.setLong(2, recipientId
.id());
1024 statement
.executeUpdate();
1028 private void storeExpiringProfileKeyCredential(
1029 final Connection connection
,
1030 final RecipientId recipientId
,
1031 final ExpiringProfileKeyCredential profileKeyCredential
1032 ) throws SQLException
{
1036 SET profile_key_credential = ?
1039 ).formatted(TABLE_RECIPIENT
);
1040 try (final var statement
= connection
.prepareStatement(sql
)) {
1041 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
1042 statement
.setLong(2, recipientId
.id());
1043 statement
.executeUpdate();
1047 public void storeProfile(
1048 final Connection connection
, final RecipientId recipientId
, final Profile profile
1049 ) throws SQLException
{
1053 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 = ?, profile_phone_number_sharing = ?
1056 ).formatted(TABLE_RECIPIENT
);
1057 try (final var statement
= connection
.prepareStatement(sql
)) {
1058 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
1059 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
1060 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
1061 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
1062 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
1063 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
1064 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
1065 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
1066 statement
.setString(9,
1069 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
1070 statement
.setString(10,
1071 profile
== null || profile
.getPhoneNumberSharingMode() == null
1073 : profile
.getPhoneNumberSharingMode().name());
1074 statement
.setLong(11, recipientId
.id());
1075 statement
.executeUpdate();
1077 rotateStorageId(connection
, recipientId
);
1080 private void storeProfileKey(
1081 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
1082 ) throws SQLException
{
1083 if (profileKey
!= null) {
1084 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
1085 if (profileKey
.equals(recipientProfileKey
)) {
1086 final var recipientProfile
= getProfile(connection
, recipientId
);
1087 if (recipientProfile
== null || (
1088 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
1089 && recipientProfile
.getUnidentifiedAccessMode()
1090 != Profile
.UnidentifiedAccessMode
.DISABLED
1100 SET profile_key = ?, profile_key_credential = NULL%s
1103 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
1104 try (final var statement
= connection
.prepareStatement(sql
)) {
1105 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
1106 statement
.setLong(2, recipientId
.id());
1107 statement
.executeUpdate();
1109 rotateStorageId(connection
, recipientId
);
1112 private RecipientAddress
resolveRecipientAddress(
1113 final Connection connection
, final RecipientId recipientId
1114 ) throws SQLException
{
1117 SELECT r.number, r.aci, r.pni, r.username
1121 ).formatted(TABLE_RECIPIENT
);
1122 try (final var statement
= connection
.prepareStatement(sql
)) {
1123 statement
.setLong(1, recipientId
.id());
1124 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
1128 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1129 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1130 try (final var connection
= database
.getConnection()) {
1131 connection
.setAutoCommit(false);
1132 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1133 connection
.commit();
1134 } catch (SQLException e
) {
1135 throw new RuntimeException("Failed update recipient store", e
);
1138 if (!pair
.second().isEmpty()) {
1139 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1140 try (final var connection
= database
.getConnection()) {
1141 connection
.setAutoCommit(false);
1142 mergeRecipients(connection
, pair
.first(), pair
.second());
1143 connection
.commit();
1144 } catch (SQLException e
) {
1145 throw new RuntimeException("Failed update recipient store", e
);
1148 return pair
.first();
1151 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1152 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1153 ) throws SQLException
{
1154 if (address
.hasSingleIdentifier() || (
1155 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1157 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1159 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1160 markRegistered(connection
, pair
.first());
1162 for (final var toBeMergedRecipientId
: pair
.second()) {
1163 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1169 private void mergeRecipients(
1170 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1171 ) throws SQLException
{
1172 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1173 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1174 deleteRecipient(connection
, toBeMergedRecipientId
);
1175 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1179 private RecipientId
resolveRecipientLocked(
1180 Connection connection
, RecipientAddress address
1181 ) throws SQLException
{
1182 final var byAci
= address
.aci().isEmpty()
1183 ? Optional
.<RecipientWithAddress
>empty()
1184 : findByServiceId(connection
, address
.aci().get());
1186 if (byAci
.isPresent()) {
1187 return byAci
.get().id();
1190 final var byPni
= address
.pni().isEmpty()
1191 ? Optional
.<RecipientWithAddress
>empty()
1192 : findByServiceId(connection
, address
.pni().get());
1194 if (byPni
.isPresent()) {
1195 return byPni
.get().id();
1198 final var byNumber
= address
.number().isEmpty()
1199 ? Optional
.<RecipientWithAddress
>empty()
1200 : findByNumber(connection
, address
.number().get());
1202 if (byNumber
.isPresent()) {
1203 return byNumber
.get().id();
1206 logger
.debug("Got new recipient, both serviceId and number are unknown");
1208 if (address
.serviceId().isEmpty()) {
1209 return addNewRecipient(connection
, address
);
1212 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1215 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1216 final var recipient
= findByServiceId(connection
, serviceId
);
1218 if (recipient
.isEmpty()) {
1219 logger
.debug("Got new recipient, serviceId is unknown");
1220 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1223 return recipient
.get().id();
1226 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1227 final var recipient
= findByNumber(connection
, number
);
1229 if (recipient
.isEmpty()) {
1230 logger
.debug("Got new recipient, number is unknown");
1231 return addNewRecipient(connection
, new RecipientAddress(number
));
1234 return recipient
.get().id();
1237 private RecipientId
addNewRecipient(
1238 final Connection connection
, final RecipientAddress address
1239 ) throws SQLException
{
1242 INSERT INTO %s (number, aci, pni, username)
1246 ).formatted(TABLE_RECIPIENT
);
1247 try (final var statement
= connection
.prepareStatement(sql
)) {
1248 statement
.setString(1, address
.number().orElse(null));
1249 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1250 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1251 statement
.setString(4, address
.username().orElse(null));
1252 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1253 if (generatedKey
.isPresent()) {
1254 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1255 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1258 throw new RuntimeException("Failed to add new recipient to database");
1263 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1264 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1268 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1271 ).formatted(TABLE_RECIPIENT
);
1272 try (final var statement
= connection
.prepareStatement(sql
)) {
1273 statement
.setLong(1, recipientId
.id());
1274 statement
.executeUpdate();
1278 private void updateRecipientAddress(
1279 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1280 ) throws SQLException
{
1281 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1285 SET number = ?, aci = ?, pni = ?, username = ?
1288 ).formatted(TABLE_RECIPIENT
);
1289 try (final var statement
= connection
.prepareStatement(sql
)) {
1290 statement
.setString(1, address
.number().orElse(null));
1291 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1292 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1293 statement
.setString(4, address
.username().orElse(null));
1294 statement
.setLong(5, recipientId
.id());
1295 statement
.executeUpdate();
1297 rotateStorageId(connection
, recipientId
);
1300 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1306 ).formatted(TABLE_RECIPIENT
);
1307 try (final var statement
= connection
.prepareStatement(sql
)) {
1308 statement
.setLong(1, recipientId
.id());
1309 statement
.executeUpdate();
1313 private void mergeRecipientsLocked(
1314 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1315 ) throws SQLException
{
1316 final var contact
= getContact(connection
, recipientId
);
1317 if (contact
== null) {
1318 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1319 storeContact(connection
, recipientId
, toBeMergedContact
);
1322 final var profileKey
= getProfileKey(connection
, recipientId
);
1323 if (profileKey
== null) {
1324 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1325 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1328 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1329 if (profileKeyCredential
== null) {
1330 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1331 toBeMergedRecipientId
);
1332 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1335 final var profile
= getProfile(connection
, recipientId
);
1336 if (profile
== null) {
1337 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1338 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1341 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1344 private Optional
<RecipientWithAddress
> findByNumber(
1345 final Connection connection
, final String number
1346 ) throws SQLException
{
1348 SELECT r._id, r.number, r.aci, r.pni, r.username
1352 """.formatted(TABLE_RECIPIENT
);
1353 try (final var statement
= connection
.prepareStatement(sql
)) {
1354 statement
.setString(1, number
);
1355 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1359 private Optional
<RecipientWithAddress
> findByUsername(
1360 final Connection connection
, final String username
1361 ) throws SQLException
{
1363 SELECT r._id, r.number, r.aci, r.pni, r.username
1365 WHERE r.username = ?
1367 """.formatted(TABLE_RECIPIENT
);
1368 try (final var statement
= connection
.prepareStatement(sql
)) {
1369 statement
.setString(1, username
);
1370 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1374 private Optional
<RecipientWithAddress
> findByServiceId(
1375 final Connection connection
, final ServiceId serviceId
1376 ) throws SQLException
{
1377 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1378 if (recipientWithAddress
.isPresent()) {
1379 return recipientWithAddress
;
1382 SELECT r._id, r.number, r.aci, r.pni, r.username
1386 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1387 try (final var statement
= connection
.prepareStatement(sql
)) {
1388 statement
.setString(1, serviceId
.toString());
1389 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1390 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1391 return recipientWithAddress
;
1395 private Set
<RecipientWithAddress
> findAllByAddress(
1396 final Connection connection
, final RecipientAddress address
1397 ) throws SQLException
{
1399 SELECT r._id, r.number, r.aci, r.pni, r.username
1405 """.formatted(TABLE_RECIPIENT
);
1406 try (final var statement
= connection
.prepareStatement(sql
)) {
1407 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1408 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1409 statement
.setString(3, address
.number().orElse(null));
1410 statement
.setString(4, address
.username().orElse(null));
1411 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1412 .collect(Collectors
.toSet());
1416 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1419 SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
1421 WHERE r._id = ? AND (%s)
1423 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1424 try (final var statement
= connection
.prepareStatement(sql
)) {
1425 statement
.setLong(1, recipientId
.id());
1426 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1430 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1431 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1432 if (recipientId
.equals(selfRecipientId
)) {
1433 return selfProfileKeyProvider
.getSelfProfileKey();
1437 SELECT r.profile_key
1441 ).formatted(TABLE_RECIPIENT
);
1442 try (final var statement
= connection
.prepareStatement(sql
)) {
1443 statement
.setLong(1, recipientId
.id());
1444 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1448 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1449 final Connection connection
, final RecipientId recipientId
1450 ) throws SQLException
{
1453 SELECT r.profile_key_credential
1457 ).formatted(TABLE_RECIPIENT
);
1458 try (final var statement
= connection
.prepareStatement(sql
)) {
1459 statement
.setLong(1, recipientId
.id());
1460 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1465 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1468 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, r.profile_phone_number_sharing
1470 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1472 ).formatted(TABLE_RECIPIENT
);
1473 try (final var statement
= connection
.prepareStatement(sql
)) {
1474 statement
.setLong(1, recipientId
.id());
1475 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1479 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1480 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrNull
);
1481 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrNull
);
1482 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1483 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1484 return new RecipientAddress(aci
, pni
, number
, username
);
1487 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1488 return new RecipientId(resultSet
.getLong("_id"), this);
1491 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1492 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1493 getRecipientAddressFromResultSet(resultSet
));
1496 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1497 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1498 getRecipientAddressFromResultSet(resultSet
),
1499 getContactFromResultSet(resultSet
),
1500 getProfileKeyFromResultSet(resultSet
),
1501 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1502 getProfileFromResultSet(resultSet
),
1503 getDiscoverableFromResultSet(resultSet
),
1504 getStorageRecordFromResultSet(resultSet
));
1507 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1508 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1509 return new Contact(resultSet
.getString("given_name"),
1510 resultSet
.getString("family_name"),
1511 resultSet
.getString("nick_name"),
1512 resultSet
.getString("nick_name_given_name"),
1513 resultSet
.getString("nick_name_family_name"),
1514 resultSet
.getString("note"),
1515 resultSet
.getString("color"),
1516 resultSet
.getInt("expiration_time"),
1517 resultSet
.getLong("mute_until"),
1518 resultSet
.getBoolean("hide_story"),
1519 resultSet
.getBoolean("blocked"),
1520 resultSet
.getBoolean("archived"),
1521 resultSet
.getBoolean("profile_sharing"),
1522 resultSet
.getBoolean("hidden"),
1523 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1526 private static Boolean
getDiscoverableFromResultSet(final ResultSet resultSet
) throws SQLException
{
1527 final var discoverable
= resultSet
.getBoolean("discoverable");
1528 if (resultSet
.wasNull()) {
1531 return discoverable
;
1534 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1535 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1536 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1537 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1538 resultSet
.getString("profile_given_name"),
1539 resultSet
.getString("profile_family_name"),
1540 resultSet
.getString("profile_about"),
1541 resultSet
.getString("profile_about_emoji"),
1542 resultSet
.getString("profile_avatar_url_path"),
1543 resultSet
.getBytes("profile_mobile_coin_address"),
1544 profileUnidentifiedAccessMode
== null
1545 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1546 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1547 profileCapabilities
== null
1549 : Arrays
.stream(profileCapabilities
.split(","))
1550 .map(Profile
.Capability
::valueOfOrNull
)
1551 .filter(Objects
::nonNull
)
1552 .collect(Collectors
.toSet()),
1553 PhoneNumberSharingMode
.valueOfOrNull(resultSet
.getString("profile_phone_number_sharing")));
1556 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1557 final var profileKey
= resultSet
.getBytes("profile_key");
1559 if (profileKey
== null) {
1563 return new ProfileKey(profileKey
);
1564 } catch (InvalidInputException ignored
) {
1569 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1570 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1572 if (profileKeyCredential
== null) {
1576 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1577 } catch (Throwable ignored
) {
1582 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1583 final var storageId
= resultSet
.getBytes("storage_id");
1584 return StorageId
.forContact(storageId
);
1587 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1588 return resultSet
.getBytes("storage_record");
1591 public interface RecipientMergeHandler
{
1593 void mergeRecipients(
1594 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1595 ) throws SQLException
;
1598 private class HelperStore
implements MergeRecipientHelper
.Store
{
1600 private final Connection connection
;
1602 public HelperStore(final Connection connection
) {
1603 this.connection
= connection
;
1607 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1608 return RecipientStore
.this.findAllByAddress(connection
, address
);
1612 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1613 return RecipientStore
.this.addNewRecipient(connection
, address
);
1617 public void updateRecipientAddress(
1618 final RecipientId recipientId
, final RecipientAddress address
1619 ) throws SQLException
{
1620 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1624 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1625 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);