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 expiration_time_version INTEGER DEFAULT 1 NOT NULL,
83 mute_until INTEGER NOT NULL DEFAULT 0,
84 blocked INTEGER NOT NULL DEFAULT FALSE,
85 archived INTEGER NOT NULL DEFAULT FALSE,
86 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
87 hide_story INTEGER NOT NULL DEFAULT FALSE,
88 hidden INTEGER NOT NULL DEFAULT FALSE,
90 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
91 profile_given_name TEXT,
92 profile_family_name TEXT,
94 profile_about_emoji TEXT,
95 profile_avatar_url_path TEXT,
96 profile_mobile_coin_address BLOB,
97 profile_unidentified_access_mode TEXT,
98 profile_capabilities TEXT,
99 profile_phone_number_sharing TEXT
105 public RecipientStore(
106 final RecipientMergeHandler recipientMergeHandler
,
107 final SelfAddressProvider selfAddressProvider
,
108 final SelfProfileKeyProvider selfProfileKeyProvider
,
109 final Database database
111 this.recipientMergeHandler
= recipientMergeHandler
;
112 this.selfAddressProvider
= selfAddressProvider
;
113 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
114 this.database
= database
;
117 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
118 try (final var connection
= database
.getConnection()) {
119 return resolveRecipientAddress(connection
, recipientId
);
120 } catch (SQLException e
) {
121 throw new RuntimeException("Failed read from recipient store", e
);
125 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
130 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
132 ).formatted(TABLE_RECIPIENT
);
133 try (final var connection
= database
.getConnection()) {
134 try (final var statement
= connection
.prepareStatement(sql
)) {
135 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
136 return result
.toList();
139 } catch (SQLException e
) {
140 throw new RuntimeException("Failed read from recipient store", e
);
145 public RecipientId
resolveRecipient(final long rawRecipientId
) {
152 ).formatted(TABLE_RECIPIENT
);
153 try (final var connection
= database
.getConnection()) {
154 try (final var statement
= connection
.prepareStatement(sql
)) {
155 statement
.setLong(1, rawRecipientId
);
156 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
158 } catch (SQLException e
) {
159 throw new RuntimeException("Failed read from recipient store", e
);
164 public RecipientId
resolveRecipient(final String identifier
) {
165 final var serviceId
= ServiceId
.parseOrNull(identifier
);
166 if (serviceId
!= null) {
167 return resolveRecipient(serviceId
);
169 return resolveRecipientByNumber(identifier
);
173 private RecipientId
resolveRecipientByNumber(final String number
) {
174 final RecipientId recipientId
;
175 try (final var connection
= database
.getConnection()) {
176 connection
.setAutoCommit(false);
177 recipientId
= resolveRecipientLocked(connection
, number
);
179 } catch (SQLException e
) {
180 throw new RuntimeException("Failed read recipient store", e
);
186 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
187 try (final var connection
= database
.getConnection()) {
188 connection
.setAutoCommit(false);
189 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
190 if (recipientWithAddress
!= null) {
191 return recipientWithAddress
.id();
193 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
196 } catch (SQLException e
) {
197 throw new RuntimeException("Failed read recipient store", e
);
202 * Should only be used for recipientIds from the database.
203 * Where the foreign key relations ensure a valid recipientId.
206 public RecipientId
create(final long recipientId
) {
207 return new RecipientId(recipientId
, this);
210 public RecipientId
resolveRecipientByNumber(
211 final String number
, Supplier
<ServiceId
> serviceIdSupplier
212 ) throws UnregisteredRecipientException
{
213 final Optional
<RecipientWithAddress
> byNumber
;
214 try (final var connection
= database
.getConnection()) {
215 byNumber
= findByNumber(connection
, number
);
216 } catch (SQLException e
) {
217 throw new RuntimeException("Failed read from recipient store", e
);
219 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
220 final var serviceId
= serviceIdSupplier
.get();
221 if (serviceId
== null) {
222 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(number
));
225 return resolveRecipient(serviceId
);
227 return byNumber
.get().id();
230 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
231 final Optional
<RecipientWithAddress
> byNumber
;
232 try (final var connection
= database
.getConnection()) {
233 byNumber
= findByNumber(connection
, number
);
234 } catch (SQLException e
) {
235 throw new RuntimeException("Failed read from recipient store", e
);
237 return byNumber
.map(RecipientWithAddress
::id
);
240 public RecipientId
resolveRecipientByUsername(
241 final String username
, Supplier
<ACI
> aciSupplier
242 ) throws UnregisteredRecipientException
{
243 final Optional
<RecipientWithAddress
> byUsername
;
244 try (final var connection
= database
.getConnection()) {
245 byUsername
= findByUsername(connection
, username
);
246 } catch (SQLException e
) {
247 throw new RuntimeException("Failed read from recipient store", e
);
249 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
250 final var aci
= aciSupplier
.get();
252 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
258 return resolveRecipientTrusted(aci
, username
);
260 return byUsername
.get().id();
263 public RecipientId
resolveRecipient(RecipientAddress address
) {
264 final RecipientId recipientId
;
265 try (final var connection
= database
.getConnection()) {
266 connection
.setAutoCommit(false);
267 recipientId
= resolveRecipientLocked(connection
, address
);
269 } catch (SQLException e
) {
270 throw new RuntimeException("Failed read recipient store", e
);
275 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
276 return resolveRecipientLocked(connection
, address
);
280 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
281 return resolveRecipientTrusted(address
, true);
285 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
286 return resolveRecipientTrusted(address
, false);
289 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
290 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
291 if (!pair
.second().isEmpty()) {
292 mergeRecipients(connection
, pair
.first(), pair
.second());
298 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
299 return resolveRecipientTrusted(new RecipientAddress(address
));
303 public RecipientId
resolveRecipientTrusted(
304 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
306 return resolveRecipientTrusted(new RecipientAddress(aci
, pni
, number
, Optional
.empty()));
310 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
311 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
315 public void storeContact(RecipientId recipientId
, final Contact contact
) {
316 try (final var connection
= database
.getConnection()) {
317 storeContact(connection
, recipientId
, contact
);
318 } catch (SQLException e
) {
319 throw new RuntimeException("Failed update recipient store", e
);
324 public Contact
getContact(RecipientId recipientId
) {
325 try (final var connection
= database
.getConnection()) {
326 return getContact(connection
, recipientId
);
327 } catch (SQLException e
) {
328 throw new RuntimeException("Failed read from recipient store", e
);
333 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
336 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.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
338 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
340 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
341 try (final var connection
= database
.getConnection()) {
342 try (final var statement
= connection
.prepareStatement(sql
)) {
343 try (var result
= Utils
.executeQueryForStream(statement
,
344 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
345 getContactFromResultSet(resultSet
)))) {
346 return result
.toList();
349 } catch (SQLException e
) {
350 throw new RuntimeException("Failed read from recipient store", e
);
354 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
358 r.number, r.aci, r.pni, r.username,
359 r.profile_key, r.profile_key_credential,
360 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.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
361 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,
367 ).formatted(TABLE_RECIPIENT
);
368 try (final var statement
= connection
.prepareStatement(sql
)) {
369 statement
.setLong(1, recipientId
.id());
370 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
374 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
378 r.number, r.aci, r.pni, r.username,
379 r.profile_key, r.profile_key_credential,
380 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.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
381 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,
385 WHERE r.storage_id = ?
387 ).formatted(TABLE_RECIPIENT
);
388 try (final var statement
= connection
.prepareStatement(sql
)) {
389 statement
.setBytes(1, storageId
.getRaw());
390 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
394 public List
<Recipient
> getRecipients(
395 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
397 final var sqlWhere
= new ArrayList
<String
>();
399 sqlWhere
.add("r.unregistered_timestamp IS NULL");
400 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
401 sqlWhere
.add("r.hidden = FALSE");
403 if (blocked
.isPresent()) {
404 sqlWhere
.add("r.blocked = ?");
406 if (!recipientIds
.isEmpty()) {
407 final var recipientIdsCommaSeparated
= recipientIds
.stream()
408 .map(recipientId
-> String
.valueOf(recipientId
.id()))
409 .collect(Collectors
.joining(","));
410 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
415 r.number, r.aci, r.pni, r.username,
416 r.profile_key, r.profile_key_credential,
417 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.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
418 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,
422 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
424 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
425 final var selfAddress
= selfAddressProvider
.getSelfAddress();
426 try (final var connection
= database
.getConnection()) {
427 try (final var statement
= connection
.prepareStatement(sql
)) {
428 if (blocked
.isPresent()) {
429 statement
.setBoolean(1, blocked
.get());
431 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
432 return result
.filter(r
-> name
.isEmpty() || (
433 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
434 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
435 if (r
.getAddress().matches(selfAddress
)) {
436 return Recipient
.newBuilder(r
)
437 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
444 } catch (SQLException e
) {
445 throw new RuntimeException("Failed read from recipient store", e
);
449 public Set
<String
> getAllNumbers() {
454 WHERE r.number IS NOT NULL
456 ).formatted(TABLE_RECIPIENT
);
457 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
458 try (final var connection
= database
.getConnection()) {
459 try (final var statement
= connection
.prepareStatement(sql
)) {
460 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
461 .filter(Objects
::nonNull
)
462 .filter(n
-> !n
.equals(selfNumber
))
467 } catch (NumberFormatException e
) {
471 .collect(Collectors
.toSet());
473 } catch (SQLException e
) {
474 throw new RuntimeException("Failed read from recipient store", e
);
478 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
481 SELECT r.aci, r.profile_key
483 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
485 ).formatted(TABLE_RECIPIENT
);
486 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
487 try (final var connection
= database
.getConnection()) {
488 try (final var statement
= connection
.prepareStatement(sql
)) {
489 return Utils
.executeQueryForStream(statement
, resultSet
-> {
490 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
491 if (aci
.equals(selfAci
)) {
492 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
494 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
495 return new Pair
<>(aci
, profileKey
);
496 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
498 } catch (SQLException e
) {
499 throw new RuntimeException("Failed read from recipient store", e
);
503 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
508 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
510 ).formatted(TABLE_RECIPIENT
);
511 try (final var statement
= connection
.prepareStatement(sql
)) {
512 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
516 public void setMissingStorageIds() {
517 final var selectSql
= (
521 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
523 ).formatted(TABLE_RECIPIENT
);
524 final var updateSql
= (
530 ).formatted(TABLE_RECIPIENT
);
531 try (final var connection
= database
.getConnection()) {
532 connection
.setAutoCommit(false);
533 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
534 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
536 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
537 for (final var recipientId
: recipientIds
) {
538 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
539 updateStmt
.setLong(2, recipientId
.id());
540 updateStmt
.executeUpdate();
545 } catch (SQLException e
) {
546 throw new RuntimeException("Failed update recipient store", e
);
551 public void deleteContact(RecipientId recipientId
) {
552 storeContact(recipientId
, null);
555 public void deleteRecipientData(RecipientId recipientId
) {
556 logger
.debug("Deleting recipient data for {}", recipientId
);
557 try (final var connection
= database
.getConnection()) {
558 connection
.setAutoCommit(false);
559 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
560 storeContact(connection
, recipientId
, null);
561 storeProfile(connection
, recipientId
, null);
562 storeProfileKey(connection
, recipientId
, null, false);
563 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
564 deleteRecipient(connection
, recipientId
);
566 } catch (SQLException e
) {
567 throw new RuntimeException("Failed update recipient store", e
);
572 public Profile
getProfile(final RecipientId recipientId
) {
573 try (final var connection
= database
.getConnection()) {
574 return getProfile(connection
, recipientId
);
575 } catch (SQLException e
) {
576 throw new RuntimeException("Failed read from recipient store", e
);
581 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
582 try (final var connection
= database
.getConnection()) {
583 return getProfileKey(connection
, recipientId
);
584 } catch (SQLException e
) {
585 throw new RuntimeException("Failed read from recipient store", e
);
590 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
591 try (final var connection
= database
.getConnection()) {
592 return getExpiringProfileKeyCredential(connection
, recipientId
);
593 } catch (SQLException e
) {
594 throw new RuntimeException("Failed read from recipient store", e
);
599 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
600 try (final var connection
= database
.getConnection()) {
601 storeProfile(connection
, recipientId
, profile
);
602 } catch (SQLException e
) {
603 throw new RuntimeException("Failed update recipient store", e
);
608 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
609 try (final var connection
= database
.getConnection()) {
610 storeProfileKey(connection
, recipientId
, profileKey
);
611 } catch (SQLException e
) {
612 throw new RuntimeException("Failed update recipient store", e
);
616 public void storeProfileKey(
617 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
618 ) throws SQLException
{
619 storeProfileKey(connection
, recipientId
, profileKey
, true);
623 public void storeExpiringProfileKeyCredential(
624 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
626 try (final var connection
= database
.getConnection()) {
627 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
628 } catch (SQLException e
) {
629 throw new RuntimeException("Failed update recipient store", e
);
633 public void rotateSelfStorageId() {
634 try (final var connection
= database
.getConnection()) {
635 rotateSelfStorageId(connection
);
636 } catch (SQLException e
) {
637 throw new RuntimeException("Failed update recipient store", e
);
641 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
642 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
643 rotateStorageId(connection
, selfRecipientId
);
646 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
647 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
648 return rotateStorageId(connection
, selfRecipientId
);
651 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
654 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)
655 """.formatted(TABLE_RECIPIENT
);
656 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
657 try (final var statement
= connection
.prepareStatement(sql
)) {
658 statement
.setLong(1, selfRecipientId
.id());
659 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
663 public void updateStorageId(
664 Connection connection
, RecipientId recipientId
, StorageId storageId
665 ) throws SQLException
{
672 ).formatted(TABLE_RECIPIENT
);
673 try (final var statement
= connection
.prepareStatement(sql
)) {
674 statement
.setBytes(1, storageId
.getRaw());
675 statement
.setLong(2, recipientId
.id());
676 statement
.executeUpdate();
680 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
687 ).formatted(TABLE_RECIPIENT
);
688 try (final var statement
= connection
.prepareStatement(sql
)) {
689 for (final var entry
: storageIdMap
.entrySet()) {
690 statement
.setBytes(1, entry
.getValue().getRaw());
691 statement
.setLong(2, entry
.getKey().id());
692 statement
.executeUpdate();
697 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
698 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
699 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
702 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
705 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
706 """.formatted(TABLE_RECIPIENT
);
707 try (final var statement
= connection
.prepareStatement(sql
)) {
708 statement
.setLong(1, recipientId
.id());
709 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
710 if (storageId
.isPresent()) {
711 return storageId
.get();
714 return rotateStorageId(connection
, recipientId
);
717 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
718 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
719 updateStorageId(connection
, recipientId
, newStorageId
);
723 public void storeStorageRecord(
724 final Connection connection
,
725 final RecipientId recipientId
,
726 final StorageId storageId
,
727 final byte[] storageRecord
728 ) throws SQLException
{
729 final var deleteSql
= (
732 SET storage_id = NULL
735 ).formatted(TABLE_RECIPIENT
);
736 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
737 statement
.setBytes(1, storageId
.getRaw());
738 statement
.executeUpdate();
740 final var insertSql
= (
743 SET storage_id = ?, storage_record = ?
746 ).formatted(TABLE_RECIPIENT
);
747 try (final var statement
= connection
.prepareStatement(insertSql
)) {
748 statement
.setBytes(1, storageId
.getRaw());
749 if (storageRecord
== null) {
750 statement
.setNull(2, Types
.BLOB
);
752 statement
.setBytes(2, storageRecord
);
754 statement
.setLong(3, recipientId
.id());
755 statement
.executeUpdate();
759 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
760 logger
.debug("Migrating legacy recipients to database");
761 long start
= System
.nanoTime();
764 INSERT INTO %s (_id, number, aci)
767 ).formatted(TABLE_RECIPIENT
);
768 try (final var connection
= database
.getConnection()) {
769 connection
.setAutoCommit(false);
770 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
771 statement
.executeUpdate();
773 try (final var statement
= connection
.prepareStatement(sql
)) {
774 for (final var recipient
: recipients
.values()) {
775 statement
.setLong(1, recipient
.getRecipientId().id());
776 statement
.setString(2, recipient
.getAddress().number().orElse(null));
777 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
778 statement
.executeUpdate();
781 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
783 for (final var recipient
: recipients
.values()) {
784 if (recipient
.getContact() != null) {
785 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
787 if (recipient
.getProfile() != null) {
788 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
790 if (recipient
.getProfileKey() != null) {
791 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
793 if (recipient
.getExpiringProfileKeyCredential() != null) {
794 storeExpiringProfileKeyCredential(connection
,
795 recipient
.getRecipientId(),
796 recipient
.getExpiringProfileKeyCredential());
800 } catch (SQLException e
) {
801 throw new RuntimeException("Failed update recipient store", e
);
803 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
806 long getActualRecipientId(long recipientId
) {
807 while (recipientsMerged
.containsKey(recipientId
)) {
808 final var newRecipientId
= recipientsMerged
.get(recipientId
);
809 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
810 recipientId
= newRecipientId
;
815 public void storeContact(
816 final Connection connection
, final RecipientId recipientId
, final Contact contact
817 ) throws SQLException
{
821 SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, expiration_time_version = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?
824 ).formatted(TABLE_RECIPIENT
);
825 try (final var statement
= connection
.prepareStatement(sql
)) {
826 statement
.setString(1, contact
== null ?
null : contact
.givenName());
827 statement
.setString(2, contact
== null ?
null : contact
.familyName());
828 statement
.setString(3, contact
== null ?
null : contact
.nickName());
829 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
830 statement
.setInt(5, contact
== null ?
0 : Math
.max(1, contact
.messageExpirationTimeVersion()));
831 statement
.setLong(6, contact
== null ?
0 : contact
.muteUntil());
832 statement
.setBoolean(7, contact
!= null && contact
.hideStory());
833 statement
.setBoolean(8, contact
!= null && contact
.isProfileSharingEnabled());
834 statement
.setString(9, contact
== null ?
null : contact
.color());
835 statement
.setBoolean(10, contact
!= null && contact
.isBlocked());
836 statement
.setBoolean(11, contact
!= null && contact
.isArchived());
837 if (contact
== null || contact
.unregisteredTimestamp() == null) {
838 statement
.setNull(12, Types
.INTEGER
);
840 statement
.setLong(12, contact
.unregisteredTimestamp());
842 statement
.setString(13, contact
== null ?
null : contact
.nickNameGivenName());
843 statement
.setString(14, contact
== null ?
null : contact
.nickNameFamilyName());
844 statement
.setString(15, contact
== null ?
null : contact
.note());
845 statement
.setLong(16, recipientId
.id());
846 statement
.executeUpdate();
848 if (contact
!= null && contact
.unregisteredTimestamp() != null) {
849 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
851 rotateStorageId(connection
, recipientId
);
854 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
855 final Connection connection
, final List
<StorageId
> storageIds
856 ) throws SQLException
{
860 SET storage_id = NULL
861 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
863 ).formatted(TABLE_RECIPIENT
);
865 try (final var statement
= connection
.prepareStatement(sql
)) {
866 for (final var storageId
: storageIds
) {
867 statement
.setBytes(1, storageId
.getRaw());
868 count
+= statement
.executeUpdate();
874 public void markNeedsPniSignature(final RecipientId recipientId
, final boolean value
) {
875 logger
.debug("Marking {} numbers as need pni signature = {}", recipientId
, value
);
876 try (final var connection
= database
.getConnection()) {
880 SET needs_pni_signature = ?
883 ).formatted(TABLE_RECIPIENT
);
884 try (final var statement
= connection
.prepareStatement(sql
)) {
885 statement
.setBoolean(1, value
);
886 statement
.setLong(2, recipientId
.id());
887 statement
.executeUpdate();
889 } catch (SQLException e
) {
890 throw new RuntimeException("Failed update recipient store", e
);
894 public boolean needsPniSignature(final RecipientId recipientId
) {
895 try (final var connection
= database
.getConnection()) {
898 SELECT needs_pni_signature
902 ).formatted(TABLE_RECIPIENT
);
903 try (final var statement
= connection
.prepareStatement(sql
)) {
904 statement
.setLong(1, recipientId
.id());
905 return Utils
.executeQuerySingleRow(statement
, resultSet
-> resultSet
.getBoolean("needs_pni_signature"));
907 } catch (SQLException e
) {
908 throw new RuntimeException("Failed read recipient store", e
);
912 public void markUndiscoverablePossiblyUnregistered(final Set
<String
> numbers
) {
913 logger
.debug("Marking {} numbers as unregistered", numbers
.size());
914 try (final var connection
= database
.getConnection()) {
915 connection
.setAutoCommit(false);
916 for (final var number
: numbers
) {
917 final var recipientAddress
= findByNumber(connection
, number
);
918 if (recipientAddress
.isPresent()) {
919 final var recipientId
= recipientAddress
.get().id();
920 markDiscoverable(connection
, recipientId
, false);
921 final var contact
= getContact(connection
, recipientId
);
922 if (recipientAddress
.get().address().aci().isEmpty() || (
923 contact
!= null && contact
.unregisteredTimestamp() != null
925 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
930 } catch (SQLException e
) {
931 throw new RuntimeException("Failed update recipient store", e
);
935 public void markDiscoverable(final Set
<String
> numbers
) {
936 logger
.debug("Marking {} numbers as discoverable", numbers
.size());
937 try (final var connection
= database
.getConnection()) {
938 connection
.setAutoCommit(false);
939 for (final var number
: numbers
) {
940 final var recipientAddress
= findByNumber(connection
, number
);
941 if (recipientAddress
.isPresent()) {
942 final var recipientId
= recipientAddress
.get().id();
943 markDiscoverable(connection
, recipientId
, true);
947 } catch (SQLException e
) {
948 throw new RuntimeException("Failed update recipient store", e
);
952 public void markRegistered(final RecipientId recipientId
, final boolean registered
) {
953 logger
.debug("Marking {} as registered={}", recipientId
, registered
);
954 try (final var connection
= database
.getConnection()) {
955 connection
.setAutoCommit(false);
957 markRegistered(connection
, recipientId
);
959 markUnregistered(connection
, recipientId
);
962 } catch (SQLException e
) {
963 throw new RuntimeException("Failed update recipient store", e
);
967 private void markUnregisteredAndSplitIfNecessary(
968 final Connection connection
, final RecipientId recipientId
969 ) throws SQLException
{
970 markUnregistered(connection
, recipientId
);
971 final var address
= resolveRecipientAddress(connection
, recipientId
);
972 if (address
.aci().isPresent() && address
.pni().isPresent()) {
973 final var numberAddress
= new RecipientAddress(address
.pni().get(), address
.number().orElse(null));
974 updateRecipientAddress(connection
, recipientId
, address
.removeIdentifiersFrom(numberAddress
));
975 addNewRecipient(connection
, numberAddress
);
979 private void markDiscoverable(
980 final Connection connection
, final RecipientId recipientId
, final boolean discoverable
981 ) throws SQLException
{
988 ).formatted(TABLE_RECIPIENT
);
989 try (final var statement
= connection
.prepareStatement(sql
)) {
990 statement
.setBoolean(1, discoverable
);
991 statement
.setLong(2, recipientId
.id());
992 statement
.executeUpdate();
996 private void markRegistered(
997 final Connection connection
, final RecipientId recipientId
998 ) throws SQLException
{
1002 SET unregistered_timestamp = NULL
1005 ).formatted(TABLE_RECIPIENT
);
1006 try (final var statement
= connection
.prepareStatement(sql
)) {
1007 statement
.setLong(1, recipientId
.id());
1008 statement
.executeUpdate();
1012 private void markUnregistered(
1013 final Connection connection
, final RecipientId recipientId
1014 ) throws SQLException
{
1018 SET unregistered_timestamp = ?, discoverable = FALSE
1021 ).formatted(TABLE_RECIPIENT
);
1022 try (final var statement
= connection
.prepareStatement(sql
)) {
1023 statement
.setLong(1, System
.currentTimeMillis());
1024 statement
.setLong(2, recipientId
.id());
1025 statement
.executeUpdate();
1029 private void storeExpiringProfileKeyCredential(
1030 final Connection connection
,
1031 final RecipientId recipientId
,
1032 final ExpiringProfileKeyCredential profileKeyCredential
1033 ) throws SQLException
{
1037 SET profile_key_credential = ?
1040 ).formatted(TABLE_RECIPIENT
);
1041 try (final var statement
= connection
.prepareStatement(sql
)) {
1042 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
1043 statement
.setLong(2, recipientId
.id());
1044 statement
.executeUpdate();
1048 public void storeProfile(
1049 final Connection connection
, final RecipientId recipientId
, final Profile profile
1050 ) throws SQLException
{
1054 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 = ?
1057 ).formatted(TABLE_RECIPIENT
);
1058 try (final var statement
= connection
.prepareStatement(sql
)) {
1059 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
1060 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
1061 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
1062 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
1063 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
1064 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
1065 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
1066 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
1067 statement
.setString(9,
1070 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
1071 statement
.setString(10,
1072 profile
== null || profile
.getPhoneNumberSharingMode() == null
1074 : profile
.getPhoneNumberSharingMode().name());
1075 statement
.setLong(11, recipientId
.id());
1076 statement
.executeUpdate();
1078 rotateStorageId(connection
, recipientId
);
1081 private void storeProfileKey(
1082 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
1083 ) throws SQLException
{
1084 if (profileKey
!= null) {
1085 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
1086 if (profileKey
.equals(recipientProfileKey
)) {
1087 final var recipientProfile
= getProfile(connection
, recipientId
);
1088 if (recipientProfile
== null || (
1089 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
1090 && recipientProfile
.getUnidentifiedAccessMode()
1091 != Profile
.UnidentifiedAccessMode
.DISABLED
1101 SET profile_key = ?, profile_key_credential = NULL%s
1104 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
1105 try (final var statement
= connection
.prepareStatement(sql
)) {
1106 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
1107 statement
.setLong(2, recipientId
.id());
1108 statement
.executeUpdate();
1110 rotateStorageId(connection
, recipientId
);
1113 private RecipientAddress
resolveRecipientAddress(
1114 final Connection connection
, final RecipientId recipientId
1115 ) throws SQLException
{
1118 SELECT r.number, r.aci, r.pni, r.username
1122 ).formatted(TABLE_RECIPIENT
);
1123 try (final var statement
= connection
.prepareStatement(sql
)) {
1124 statement
.setLong(1, recipientId
.id());
1125 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
1129 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1130 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1131 try (final var connection
= database
.getConnection()) {
1132 connection
.setAutoCommit(false);
1133 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1134 connection
.commit();
1135 } catch (SQLException e
) {
1136 throw new RuntimeException("Failed update recipient store", e
);
1139 if (!pair
.second().isEmpty()) {
1140 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1141 try (final var connection
= database
.getConnection()) {
1142 connection
.setAutoCommit(false);
1143 mergeRecipients(connection
, pair
.first(), pair
.second());
1144 connection
.commit();
1145 } catch (SQLException e
) {
1146 throw new RuntimeException("Failed update recipient store", e
);
1149 return pair
.first();
1152 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1153 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1154 ) throws SQLException
{
1155 if (address
.hasSingleIdentifier() || (
1156 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1158 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1160 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1161 markRegistered(connection
, pair
.first());
1163 for (final var toBeMergedRecipientId
: pair
.second()) {
1164 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1170 private void mergeRecipients(
1171 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1172 ) throws SQLException
{
1173 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1174 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1175 deleteRecipient(connection
, toBeMergedRecipientId
);
1176 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1180 private RecipientId
resolveRecipientLocked(
1181 Connection connection
, RecipientAddress address
1182 ) throws SQLException
{
1183 final var byAci
= address
.aci().isEmpty()
1184 ? Optional
.<RecipientWithAddress
>empty()
1185 : findByServiceId(connection
, address
.aci().get());
1187 if (byAci
.isPresent()) {
1188 return byAci
.get().id();
1191 final var byPni
= address
.pni().isEmpty()
1192 ? Optional
.<RecipientWithAddress
>empty()
1193 : findByServiceId(connection
, address
.pni().get());
1195 if (byPni
.isPresent()) {
1196 return byPni
.get().id();
1199 final var byNumber
= address
.number().isEmpty()
1200 ? Optional
.<RecipientWithAddress
>empty()
1201 : findByNumber(connection
, address
.number().get());
1203 if (byNumber
.isPresent()) {
1204 return byNumber
.get().id();
1207 logger
.debug("Got new recipient, both serviceId and number are unknown");
1209 if (address
.serviceId().isEmpty()) {
1210 return addNewRecipient(connection
, address
);
1213 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1216 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1217 final var recipient
= findByServiceId(connection
, serviceId
);
1219 if (recipient
.isEmpty()) {
1220 logger
.debug("Got new recipient, serviceId is unknown");
1221 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1224 return recipient
.get().id();
1227 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1228 final var recipient
= findByNumber(connection
, number
);
1230 if (recipient
.isEmpty()) {
1231 logger
.debug("Got new recipient, number is unknown");
1232 return addNewRecipient(connection
, new RecipientAddress(number
));
1235 return recipient
.get().id();
1238 private RecipientId
addNewRecipient(
1239 final Connection connection
, final RecipientAddress address
1240 ) throws SQLException
{
1243 INSERT INTO %s (number, aci, pni, username)
1247 ).formatted(TABLE_RECIPIENT
);
1248 try (final var statement
= connection
.prepareStatement(sql
)) {
1249 statement
.setString(1, address
.number().orElse(null));
1250 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1251 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1252 statement
.setString(4, address
.username().orElse(null));
1253 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1254 if (generatedKey
.isPresent()) {
1255 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1256 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1259 throw new RuntimeException("Failed to add new recipient to database");
1264 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1265 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1269 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1272 ).formatted(TABLE_RECIPIENT
);
1273 try (final var statement
= connection
.prepareStatement(sql
)) {
1274 statement
.setLong(1, recipientId
.id());
1275 statement
.executeUpdate();
1279 private void updateRecipientAddress(
1280 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1281 ) throws SQLException
{
1282 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1286 SET number = ?, aci = ?, pni = ?, username = ?
1289 ).formatted(TABLE_RECIPIENT
);
1290 try (final var statement
= connection
.prepareStatement(sql
)) {
1291 statement
.setString(1, address
.number().orElse(null));
1292 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1293 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1294 statement
.setString(4, address
.username().orElse(null));
1295 statement
.setLong(5, recipientId
.id());
1296 statement
.executeUpdate();
1298 rotateStorageId(connection
, recipientId
);
1301 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1307 ).formatted(TABLE_RECIPIENT
);
1308 try (final var statement
= connection
.prepareStatement(sql
)) {
1309 statement
.setLong(1, recipientId
.id());
1310 statement
.executeUpdate();
1314 private void mergeRecipientsLocked(
1315 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1316 ) throws SQLException
{
1317 final var contact
= getContact(connection
, recipientId
);
1318 if (contact
== null) {
1319 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1320 storeContact(connection
, recipientId
, toBeMergedContact
);
1323 final var profileKey
= getProfileKey(connection
, recipientId
);
1324 if (profileKey
== null) {
1325 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1326 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1329 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1330 if (profileKeyCredential
== null) {
1331 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1332 toBeMergedRecipientId
);
1333 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1336 final var profile
= getProfile(connection
, recipientId
);
1337 if (profile
== null) {
1338 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1339 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1342 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1345 private Optional
<RecipientWithAddress
> findByNumber(
1346 final Connection connection
, final String number
1347 ) throws SQLException
{
1349 SELECT r._id, r.number, r.aci, r.pni, r.username
1353 """.formatted(TABLE_RECIPIENT
);
1354 try (final var statement
= connection
.prepareStatement(sql
)) {
1355 statement
.setString(1, number
);
1356 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1360 private Optional
<RecipientWithAddress
> findByUsername(
1361 final Connection connection
, final String username
1362 ) throws SQLException
{
1364 SELECT r._id, r.number, r.aci, r.pni, r.username
1366 WHERE r.username = ?
1368 """.formatted(TABLE_RECIPIENT
);
1369 try (final var statement
= connection
.prepareStatement(sql
)) {
1370 statement
.setString(1, username
);
1371 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1375 private Optional
<RecipientWithAddress
> findByServiceId(
1376 final Connection connection
, final ServiceId serviceId
1377 ) throws SQLException
{
1378 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1379 if (recipientWithAddress
.isPresent()) {
1380 return recipientWithAddress
;
1383 SELECT r._id, r.number, r.aci, r.pni, r.username
1387 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1388 try (final var statement
= connection
.prepareStatement(sql
)) {
1389 statement
.setString(1, serviceId
.toString());
1390 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1391 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1392 return recipientWithAddress
;
1396 private Set
<RecipientWithAddress
> findAllByAddress(
1397 final Connection connection
, final RecipientAddress address
1398 ) throws SQLException
{
1400 SELECT r._id, r.number, r.aci, r.pni, r.username
1406 """.formatted(TABLE_RECIPIENT
);
1407 try (final var statement
= connection
.prepareStatement(sql
)) {
1408 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1409 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1410 statement
.setString(3, address
.number().orElse(null));
1411 statement
.setString(4, address
.username().orElse(null));
1412 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1413 .collect(Collectors
.toSet());
1417 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1420 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.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
1422 WHERE r._id = ? AND (%s)
1424 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1425 try (final var statement
= connection
.prepareStatement(sql
)) {
1426 statement
.setLong(1, recipientId
.id());
1427 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1431 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1432 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1433 if (recipientId
.equals(selfRecipientId
)) {
1434 return selfProfileKeyProvider
.getSelfProfileKey();
1438 SELECT r.profile_key
1442 ).formatted(TABLE_RECIPIENT
);
1443 try (final var statement
= connection
.prepareStatement(sql
)) {
1444 statement
.setLong(1, recipientId
.id());
1445 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1449 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1450 final Connection connection
, final RecipientId recipientId
1451 ) throws SQLException
{
1454 SELECT r.profile_key_credential
1458 ).formatted(TABLE_RECIPIENT
);
1459 try (final var statement
= connection
.prepareStatement(sql
)) {
1460 statement
.setLong(1, recipientId
.id());
1461 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1466 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1469 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
1471 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1473 ).formatted(TABLE_RECIPIENT
);
1474 try (final var statement
= connection
.prepareStatement(sql
)) {
1475 statement
.setLong(1, recipientId
.id());
1476 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1480 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1481 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrNull
);
1482 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrNull
);
1483 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1484 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1485 return new RecipientAddress(aci
, pni
, number
, username
);
1488 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1489 return new RecipientId(resultSet
.getLong("_id"), this);
1492 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1493 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1494 getRecipientAddressFromResultSet(resultSet
));
1497 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1498 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1499 getRecipientAddressFromResultSet(resultSet
),
1500 getContactFromResultSet(resultSet
),
1501 getProfileKeyFromResultSet(resultSet
),
1502 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1503 getProfileFromResultSet(resultSet
),
1504 getDiscoverableFromResultSet(resultSet
),
1505 getStorageRecordFromResultSet(resultSet
));
1508 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1509 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1510 return new Contact(resultSet
.getString("given_name"),
1511 resultSet
.getString("family_name"),
1512 resultSet
.getString("nick_name"),
1513 resultSet
.getString("nick_name_given_name"),
1514 resultSet
.getString("nick_name_family_name"),
1515 resultSet
.getString("note"),
1516 resultSet
.getString("color"),
1517 resultSet
.getInt("expiration_time"),
1518 resultSet
.getInt("expiration_time_version"),
1519 resultSet
.getLong("mute_until"),
1520 resultSet
.getBoolean("hide_story"),
1521 resultSet
.getBoolean("blocked"),
1522 resultSet
.getBoolean("archived"),
1523 resultSet
.getBoolean("profile_sharing"),
1524 resultSet
.getBoolean("hidden"),
1525 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1528 private static Boolean
getDiscoverableFromResultSet(final ResultSet resultSet
) throws SQLException
{
1529 final var discoverable
= resultSet
.getBoolean("discoverable");
1530 if (resultSet
.wasNull()) {
1533 return discoverable
;
1536 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1537 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1538 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1539 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1540 resultSet
.getString("profile_given_name"),
1541 resultSet
.getString("profile_family_name"),
1542 resultSet
.getString("profile_about"),
1543 resultSet
.getString("profile_about_emoji"),
1544 resultSet
.getString("profile_avatar_url_path"),
1545 resultSet
.getBytes("profile_mobile_coin_address"),
1546 profileUnidentifiedAccessMode
== null
1547 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1548 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1549 profileCapabilities
== null
1551 : Arrays
.stream(profileCapabilities
.split(","))
1552 .map(Profile
.Capability
::valueOfOrNull
)
1553 .filter(Objects
::nonNull
)
1554 .collect(Collectors
.toSet()),
1555 PhoneNumberSharingMode
.valueOfOrNull(resultSet
.getString("profile_phone_number_sharing")));
1558 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1559 final var profileKey
= resultSet
.getBytes("profile_key");
1561 if (profileKey
== null) {
1565 return new ProfileKey(profileKey
);
1566 } catch (InvalidInputException ignored
) {
1571 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1572 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1574 if (profileKeyCredential
== null) {
1578 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1579 } catch (Throwable ignored
) {
1584 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1585 final var storageId
= resultSet
.getBytes("storage_id");
1586 return StorageId
.forContact(storageId
);
1589 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1590 return resultSet
.getBytes("storage_record");
1593 public interface RecipientMergeHandler
{
1595 void mergeRecipients(
1596 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1597 ) throws SQLException
;
1600 private class HelperStore
implements MergeRecipientHelper
.Store
{
1602 private final Connection connection
;
1604 public HelperStore(final Connection connection
) {
1605 this.connection
= connection
;
1609 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1610 return RecipientStore
.this.findAllByAddress(connection
, address
);
1614 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1615 return RecipientStore
.this.addNewRecipient(connection
, address
);
1619 public void updateRecipientAddress(
1620 final RecipientId recipientId
, final RecipientAddress address
1621 ) throws SQLException
{
1622 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1626 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1627 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);