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(
212 Supplier
<ServiceId
> serviceIdSupplier
213 ) throws UnregisteredRecipientException
{
214 final Optional
<RecipientWithAddress
> byNumber
;
215 try (final var connection
= database
.getConnection()) {
216 byNumber
= findByNumber(connection
, number
);
217 } catch (SQLException e
) {
218 throw new RuntimeException("Failed read from recipient store", e
);
220 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
221 final var serviceId
= serviceIdSupplier
.get();
222 if (serviceId
== null) {
223 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(number
));
226 return resolveRecipient(serviceId
);
228 return byNumber
.get().id();
231 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
232 final Optional
<RecipientWithAddress
> byNumber
;
233 try (final var connection
= database
.getConnection()) {
234 byNumber
= findByNumber(connection
, number
);
235 } catch (SQLException e
) {
236 throw new RuntimeException("Failed read from recipient store", e
);
238 return byNumber
.map(RecipientWithAddress
::id
);
241 public RecipientId
resolveRecipientByUsername(
242 final String username
,
243 Supplier
<ACI
> aciSupplier
244 ) throws UnregisteredRecipientException
{
245 final Optional
<RecipientWithAddress
> byUsername
;
246 try (final var connection
= database
.getConnection()) {
247 byUsername
= findByUsername(connection
, username
);
248 } catch (SQLException e
) {
249 throw new RuntimeException("Failed read from recipient store", e
);
251 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
252 final var aci
= aciSupplier
.get();
254 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
260 return resolveRecipientTrusted(aci
, username
);
262 return byUsername
.get().id();
265 public RecipientId
resolveRecipient(RecipientAddress address
) {
266 final RecipientId recipientId
;
267 try (final var connection
= database
.getConnection()) {
268 connection
.setAutoCommit(false);
269 recipientId
= resolveRecipientLocked(connection
, address
);
271 } catch (SQLException e
) {
272 throw new RuntimeException("Failed read recipient store", e
);
277 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
278 return resolveRecipientLocked(connection
, address
);
282 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
283 return resolveRecipientTrusted(address
, true);
287 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
288 return resolveRecipientTrusted(address
, false);
291 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
292 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
293 if (!pair
.second().isEmpty()) {
294 mergeRecipients(connection
, pair
.first(), pair
.second());
300 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
301 return resolveRecipientTrusted(new RecipientAddress(address
));
305 public RecipientId
resolveRecipientTrusted(
306 final Optional
<ACI
> aci
,
307 final Optional
<PNI
> pni
,
308 final Optional
<String
> number
310 return resolveRecipientTrusted(new RecipientAddress(aci
, pni
, number
, Optional
.empty()));
314 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
315 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
319 public void storeContact(RecipientId recipientId
, final Contact contact
) {
320 try (final var connection
= database
.getConnection()) {
321 storeContact(connection
, recipientId
, contact
);
322 } catch (SQLException e
) {
323 throw new RuntimeException("Failed update recipient store", e
);
328 public Contact
getContact(RecipientId recipientId
) {
329 try (final var connection
= database
.getConnection()) {
330 return getContact(connection
, recipientId
);
331 } catch (SQLException e
) {
332 throw new RuntimeException("Failed read from recipient store", e
);
337 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
340 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
342 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
344 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
345 try (final var connection
= database
.getConnection()) {
346 try (final var statement
= connection
.prepareStatement(sql
)) {
347 try (var result
= Utils
.executeQueryForStream(statement
,
348 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
349 getContactFromResultSet(resultSet
)))) {
350 return result
.toList();
353 } catch (SQLException e
) {
354 throw new RuntimeException("Failed read from recipient store", e
);
358 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
362 r.number, r.aci, r.pni, r.username,
363 r.profile_key, r.profile_key_credential,
364 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,
365 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,
371 ).formatted(TABLE_RECIPIENT
);
372 try (final var statement
= connection
.prepareStatement(sql
)) {
373 statement
.setLong(1, recipientId
.id());
374 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
378 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
382 r.number, r.aci, r.pni, r.username,
383 r.profile_key, r.profile_key_credential,
384 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,
385 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,
389 WHERE r.storage_id = ?
391 ).formatted(TABLE_RECIPIENT
);
392 try (final var statement
= connection
.prepareStatement(sql
)) {
393 statement
.setBytes(1, storageId
.getRaw());
394 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
398 public List
<Recipient
> getRecipients(
399 boolean onlyContacts
,
400 Optional
<Boolean
> blocked
,
401 Set
<RecipientId
> recipientIds
,
402 Optional
<String
> name
404 final var sqlWhere
= new ArrayList
<String
>();
406 sqlWhere
.add("r.unregistered_timestamp IS NULL");
407 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
408 sqlWhere
.add("r.hidden = FALSE");
410 if (blocked
.isPresent()) {
411 sqlWhere
.add("r.blocked = ?");
413 if (!recipientIds
.isEmpty()) {
414 final var recipientIdsCommaSeparated
= recipientIds
.stream()
415 .map(recipientId
-> String
.valueOf(recipientId
.id()))
416 .collect(Collectors
.joining(","));
417 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
422 r.number, r.aci, r.pni, r.username,
423 r.profile_key, r.profile_key_credential,
424 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,
425 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,
429 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
431 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
432 final var selfAddress
= selfAddressProvider
.getSelfAddress();
433 try (final var connection
= database
.getConnection()) {
434 try (final var statement
= connection
.prepareStatement(sql
)) {
435 if (blocked
.isPresent()) {
436 statement
.setBoolean(1, blocked
.get());
438 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
439 return result
.filter(r
-> name
.isEmpty() || (
440 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
441 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
442 if (r
.getAddress().matches(selfAddress
)) {
443 return Recipient
.newBuilder(r
)
444 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
451 } catch (SQLException e
) {
452 throw new RuntimeException("Failed read from recipient store", e
);
456 public Set
<String
> getAllNumbers() {
461 WHERE r.number IS NOT NULL
463 ).formatted(TABLE_RECIPIENT
);
464 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
465 try (final var connection
= database
.getConnection()) {
466 try (final var statement
= connection
.prepareStatement(sql
)) {
467 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
468 .filter(Objects
::nonNull
)
469 .filter(n
-> !n
.equals(selfNumber
))
474 } catch (NumberFormatException e
) {
478 .collect(Collectors
.toSet());
480 } catch (SQLException e
) {
481 throw new RuntimeException("Failed read from recipient store", e
);
485 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
488 SELECT r.aci, r.profile_key
490 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
492 ).formatted(TABLE_RECIPIENT
);
493 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
494 try (final var connection
= database
.getConnection()) {
495 try (final var statement
= connection
.prepareStatement(sql
)) {
496 return Utils
.executeQueryForStream(statement
, resultSet
-> {
497 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
498 if (aci
.equals(selfAci
)) {
499 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
501 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
502 return new Pair
<>(aci
, profileKey
);
503 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
505 } catch (SQLException e
) {
506 throw new RuntimeException("Failed read from recipient store", e
);
510 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
515 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
517 ).formatted(TABLE_RECIPIENT
);
518 try (final var statement
= connection
.prepareStatement(sql
)) {
519 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
523 public void setMissingStorageIds() {
524 final var selectSql
= (
528 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
530 ).formatted(TABLE_RECIPIENT
);
531 final var updateSql
= (
537 ).formatted(TABLE_RECIPIENT
);
538 try (final var connection
= database
.getConnection()) {
539 connection
.setAutoCommit(false);
540 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
541 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
543 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
544 for (final var recipientId
: recipientIds
) {
545 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
546 updateStmt
.setLong(2, recipientId
.id());
547 updateStmt
.executeUpdate();
552 } catch (SQLException e
) {
553 throw new RuntimeException("Failed update recipient store", e
);
558 public void deleteContact(RecipientId recipientId
) {
559 storeContact(recipientId
, null);
562 public void deleteRecipientData(RecipientId recipientId
) {
563 logger
.debug("Deleting recipient data for {}", recipientId
);
564 try (final var connection
= database
.getConnection()) {
565 connection
.setAutoCommit(false);
566 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
567 storeContact(connection
, recipientId
, null);
568 storeProfile(connection
, recipientId
, null);
569 storeProfileKey(connection
, recipientId
, null, false);
570 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
571 deleteRecipient(connection
, recipientId
);
573 } catch (SQLException e
) {
574 throw new RuntimeException("Failed update recipient store", e
);
579 public Profile
getProfile(final RecipientId recipientId
) {
580 try (final var connection
= database
.getConnection()) {
581 return getProfile(connection
, recipientId
);
582 } catch (SQLException e
) {
583 throw new RuntimeException("Failed read from recipient store", e
);
588 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
589 try (final var connection
= database
.getConnection()) {
590 return getProfileKey(connection
, recipientId
);
591 } catch (SQLException e
) {
592 throw new RuntimeException("Failed read from recipient store", e
);
597 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
598 try (final var connection
= database
.getConnection()) {
599 return getExpiringProfileKeyCredential(connection
, recipientId
);
600 } catch (SQLException e
) {
601 throw new RuntimeException("Failed read from recipient store", e
);
606 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
607 try (final var connection
= database
.getConnection()) {
608 storeProfile(connection
, recipientId
, profile
);
609 } catch (SQLException e
) {
610 throw new RuntimeException("Failed update recipient store", e
);
615 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
616 try (final var connection
= database
.getConnection()) {
617 storeProfileKey(connection
, recipientId
, profileKey
);
618 } catch (SQLException e
) {
619 throw new RuntimeException("Failed update recipient store", e
);
623 public void storeProfileKey(
624 Connection connection
,
625 RecipientId recipientId
,
626 final ProfileKey profileKey
627 ) throws SQLException
{
628 storeProfileKey(connection
, recipientId
, profileKey
, true);
632 public void storeExpiringProfileKeyCredential(
633 RecipientId recipientId
,
634 final ExpiringProfileKeyCredential profileKeyCredential
636 try (final var connection
= database
.getConnection()) {
637 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
638 } catch (SQLException e
) {
639 throw new RuntimeException("Failed update recipient store", e
);
643 public void rotateSelfStorageId() {
644 try (final var connection
= database
.getConnection()) {
645 rotateSelfStorageId(connection
);
646 } catch (SQLException e
) {
647 throw new RuntimeException("Failed update recipient store", e
);
651 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
652 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
653 rotateStorageId(connection
, selfRecipientId
);
656 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
657 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
658 return rotateStorageId(connection
, selfRecipientId
);
661 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
664 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)
665 """.formatted(TABLE_RECIPIENT
);
666 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
667 try (final var statement
= connection
.prepareStatement(sql
)) {
668 statement
.setLong(1, selfRecipientId
.id());
669 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
673 public void updateStorageId(
674 Connection connection
,
675 RecipientId recipientId
,
677 ) throws SQLException
{
684 ).formatted(TABLE_RECIPIENT
);
685 try (final var statement
= connection
.prepareStatement(sql
)) {
686 statement
.setBytes(1, storageId
.getRaw());
687 statement
.setLong(2, recipientId
.id());
688 statement
.executeUpdate();
692 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
699 ).formatted(TABLE_RECIPIENT
);
700 try (final var statement
= connection
.prepareStatement(sql
)) {
701 for (final var entry
: storageIdMap
.entrySet()) {
702 statement
.setBytes(1, entry
.getValue().getRaw());
703 statement
.setLong(2, entry
.getKey().id());
704 statement
.executeUpdate();
709 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
710 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
711 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
714 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
717 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
718 """.formatted(TABLE_RECIPIENT
);
719 try (final var statement
= connection
.prepareStatement(sql
)) {
720 statement
.setLong(1, recipientId
.id());
721 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
722 if (storageId
.isPresent()) {
723 return storageId
.get();
726 return rotateStorageId(connection
, recipientId
);
729 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
730 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
731 updateStorageId(connection
, recipientId
, newStorageId
);
735 public void storeStorageRecord(
736 final Connection connection
,
737 final RecipientId recipientId
,
738 final StorageId storageId
,
739 final byte[] storageRecord
740 ) throws SQLException
{
741 final var deleteSql
= (
744 SET storage_id = NULL
747 ).formatted(TABLE_RECIPIENT
);
748 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
749 statement
.setBytes(1, storageId
.getRaw());
750 statement
.executeUpdate();
752 final var insertSql
= (
755 SET storage_id = ?, storage_record = ?
758 ).formatted(TABLE_RECIPIENT
);
759 try (final var statement
= connection
.prepareStatement(insertSql
)) {
760 statement
.setBytes(1, storageId
.getRaw());
761 if (storageRecord
== null) {
762 statement
.setNull(2, Types
.BLOB
);
764 statement
.setBytes(2, storageRecord
);
766 statement
.setLong(3, recipientId
.id());
767 statement
.executeUpdate();
771 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
772 logger
.debug("Migrating legacy recipients to database");
773 long start
= System
.nanoTime();
776 INSERT INTO %s (_id, number, aci)
779 ).formatted(TABLE_RECIPIENT
);
780 try (final var connection
= database
.getConnection()) {
781 connection
.setAutoCommit(false);
782 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
783 statement
.executeUpdate();
785 try (final var statement
= connection
.prepareStatement(sql
)) {
786 for (final var recipient
: recipients
.values()) {
787 statement
.setLong(1, recipient
.getRecipientId().id());
788 statement
.setString(2, recipient
.getAddress().number().orElse(null));
789 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
790 statement
.executeUpdate();
793 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
795 for (final var recipient
: recipients
.values()) {
796 if (recipient
.getContact() != null) {
797 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
799 if (recipient
.getProfile() != null) {
800 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
802 if (recipient
.getProfileKey() != null) {
803 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
805 if (recipient
.getExpiringProfileKeyCredential() != null) {
806 storeExpiringProfileKeyCredential(connection
,
807 recipient
.getRecipientId(),
808 recipient
.getExpiringProfileKeyCredential());
812 } catch (SQLException e
) {
813 throw new RuntimeException("Failed update recipient store", e
);
815 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
818 long getActualRecipientId(long recipientId
) {
819 while (recipientsMerged
.containsKey(recipientId
)) {
820 final var newRecipientId
= recipientsMerged
.get(recipientId
);
821 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
822 recipientId
= newRecipientId
;
827 public void storeContact(
828 final Connection connection
,
829 final RecipientId recipientId
,
830 final Contact contact
831 ) throws SQLException
{
835 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 = ?
838 ).formatted(TABLE_RECIPIENT
);
839 try (final var statement
= connection
.prepareStatement(sql
)) {
840 statement
.setString(1, contact
== null ?
null : contact
.givenName());
841 statement
.setString(2, contact
== null ?
null : contact
.familyName());
842 statement
.setString(3, contact
== null ?
null : contact
.nickName());
843 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
844 statement
.setInt(5, contact
== null ?
0 : Math
.max(1, contact
.messageExpirationTimeVersion()));
845 statement
.setLong(6, contact
== null ?
0 : contact
.muteUntil());
846 statement
.setBoolean(7, contact
!= null && contact
.hideStory());
847 statement
.setBoolean(8, contact
!= null && contact
.isProfileSharingEnabled());
848 statement
.setString(9, contact
== null ?
null : contact
.color());
849 statement
.setBoolean(10, contact
!= null && contact
.isBlocked());
850 statement
.setBoolean(11, contact
!= null && contact
.isArchived());
851 if (contact
== null || contact
.unregisteredTimestamp() == null) {
852 statement
.setNull(12, Types
.INTEGER
);
854 statement
.setLong(12, contact
.unregisteredTimestamp());
856 statement
.setString(13, contact
== null ?
null : contact
.nickNameGivenName());
857 statement
.setString(14, contact
== null ?
null : contact
.nickNameFamilyName());
858 statement
.setString(15, contact
== null ?
null : contact
.note());
859 statement
.setLong(16, recipientId
.id());
860 statement
.executeUpdate();
862 if (contact
!= null && contact
.unregisteredTimestamp() != null) {
863 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
865 rotateStorageId(connection
, recipientId
);
868 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
869 final Connection connection
,
870 final List
<StorageId
> storageIds
871 ) throws SQLException
{
875 SET storage_id = NULL
876 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
878 ).formatted(TABLE_RECIPIENT
);
880 try (final var statement
= connection
.prepareStatement(sql
)) {
881 for (final var storageId
: storageIds
) {
882 statement
.setBytes(1, storageId
.getRaw());
883 count
+= statement
.executeUpdate();
889 public void markNeedsPniSignature(final RecipientId recipientId
, final boolean value
) {
890 logger
.debug("Marking {} numbers as need pni signature = {}", recipientId
, value
);
891 try (final var connection
= database
.getConnection()) {
895 SET needs_pni_signature = ?
898 ).formatted(TABLE_RECIPIENT
);
899 try (final var statement
= connection
.prepareStatement(sql
)) {
900 statement
.setBoolean(1, value
);
901 statement
.setLong(2, recipientId
.id());
902 statement
.executeUpdate();
904 } catch (SQLException e
) {
905 throw new RuntimeException("Failed update recipient store", e
);
909 public boolean needsPniSignature(final RecipientId recipientId
) {
910 try (final var connection
= database
.getConnection()) {
913 SELECT needs_pni_signature
917 ).formatted(TABLE_RECIPIENT
);
918 try (final var statement
= connection
.prepareStatement(sql
)) {
919 statement
.setLong(1, recipientId
.id());
920 return Utils
.executeQuerySingleRow(statement
, resultSet
-> resultSet
.getBoolean("needs_pni_signature"));
922 } catch (SQLException e
) {
923 throw new RuntimeException("Failed read recipient store", e
);
927 public void markUndiscoverablePossiblyUnregistered(final Set
<String
> numbers
) {
928 logger
.debug("Marking {} numbers as unregistered", numbers
.size());
929 try (final var connection
= database
.getConnection()) {
930 connection
.setAutoCommit(false);
931 for (final var number
: numbers
) {
932 final var recipientAddress
= findByNumber(connection
, number
);
933 if (recipientAddress
.isPresent()) {
934 final var recipientId
= recipientAddress
.get().id();
935 markDiscoverable(connection
, recipientId
, false);
936 final var contact
= getContact(connection
, recipientId
);
937 if (recipientAddress
.get().address().aci().isEmpty() || (
938 contact
!= null && contact
.unregisteredTimestamp() != null
940 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
945 } catch (SQLException e
) {
946 throw new RuntimeException("Failed update recipient store", e
);
950 public void markDiscoverable(final Set
<String
> numbers
) {
951 logger
.debug("Marking {} numbers as discoverable", numbers
.size());
952 try (final var connection
= database
.getConnection()) {
953 connection
.setAutoCommit(false);
954 for (final var number
: numbers
) {
955 final var recipientAddress
= findByNumber(connection
, number
);
956 if (recipientAddress
.isPresent()) {
957 final var recipientId
= recipientAddress
.get().id();
958 markDiscoverable(connection
, recipientId
, true);
962 } catch (SQLException e
) {
963 throw new RuntimeException("Failed update recipient store", e
);
967 public void markRegistered(final RecipientId recipientId
, final boolean registered
) {
968 logger
.debug("Marking {} as registered={}", recipientId
, registered
);
969 try (final var connection
= database
.getConnection()) {
970 connection
.setAutoCommit(false);
972 markRegistered(connection
, recipientId
);
974 markUnregistered(connection
, recipientId
);
977 } catch (SQLException e
) {
978 throw new RuntimeException("Failed update recipient store", e
);
982 private void markUnregisteredAndSplitIfNecessary(
983 final Connection connection
,
984 final RecipientId recipientId
985 ) throws SQLException
{
986 markUnregistered(connection
, recipientId
);
987 final var address
= resolveRecipientAddress(connection
, recipientId
);
988 if (address
.aci().isPresent() && address
.pni().isPresent()) {
989 final var numberAddress
= new RecipientAddress(address
.pni().get(), address
.number().orElse(null));
990 updateRecipientAddress(connection
, recipientId
, address
.removeIdentifiersFrom(numberAddress
));
991 addNewRecipient(connection
, numberAddress
);
995 private void markDiscoverable(
996 final Connection connection
,
997 final RecipientId recipientId
,
998 final boolean discoverable
999 ) throws SQLException
{
1003 SET discoverable = ?
1006 ).formatted(TABLE_RECIPIENT
);
1007 try (final var statement
= connection
.prepareStatement(sql
)) {
1008 statement
.setBoolean(1, discoverable
);
1009 statement
.setLong(2, recipientId
.id());
1010 statement
.executeUpdate();
1014 private void markRegistered(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1018 SET unregistered_timestamp = NULL
1021 ).formatted(TABLE_RECIPIENT
);
1022 try (final var statement
= connection
.prepareStatement(sql
)) {
1023 statement
.setLong(1, recipientId
.id());
1024 statement
.executeUpdate();
1028 private void markUnregistered(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1032 SET unregistered_timestamp = ?, discoverable = FALSE
1035 ).formatted(TABLE_RECIPIENT
);
1036 try (final var statement
= connection
.prepareStatement(sql
)) {
1037 statement
.setLong(1, System
.currentTimeMillis());
1038 statement
.setLong(2, recipientId
.id());
1039 statement
.executeUpdate();
1043 private void storeExpiringProfileKeyCredential(
1044 final Connection connection
,
1045 final RecipientId recipientId
,
1046 final ExpiringProfileKeyCredential profileKeyCredential
1047 ) throws SQLException
{
1051 SET profile_key_credential = ?
1054 ).formatted(TABLE_RECIPIENT
);
1055 try (final var statement
= connection
.prepareStatement(sql
)) {
1056 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
1057 statement
.setLong(2, recipientId
.id());
1058 statement
.executeUpdate();
1062 public void storeProfile(
1063 final Connection connection
,
1064 final RecipientId recipientId
,
1065 final Profile profile
1066 ) throws SQLException
{
1070 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 = ?
1073 ).formatted(TABLE_RECIPIENT
);
1074 try (final var statement
= connection
.prepareStatement(sql
)) {
1075 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
1076 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
1077 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
1078 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
1079 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
1080 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
1081 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
1082 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
1083 statement
.setString(9,
1086 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
1087 statement
.setString(10,
1088 profile
== null || profile
.getPhoneNumberSharingMode() == null
1090 : profile
.getPhoneNumberSharingMode().name());
1091 statement
.setLong(11, recipientId
.id());
1092 statement
.executeUpdate();
1094 rotateStorageId(connection
, recipientId
);
1097 private void storeProfileKey(
1098 Connection connection
,
1099 RecipientId recipientId
,
1100 final ProfileKey profileKey
,
1101 boolean resetProfile
1102 ) throws SQLException
{
1103 if (profileKey
!= null) {
1104 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
1105 if (profileKey
.equals(recipientProfileKey
)) {
1106 final var recipientProfile
= getProfile(connection
, recipientId
);
1107 if (recipientProfile
== null || (
1108 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
1109 && recipientProfile
.getUnidentifiedAccessMode()
1110 != Profile
.UnidentifiedAccessMode
.DISABLED
1120 SET profile_key = ?, profile_key_credential = NULL%s
1123 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
1124 try (final var statement
= connection
.prepareStatement(sql
)) {
1125 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
1126 statement
.setLong(2, recipientId
.id());
1127 statement
.executeUpdate();
1129 rotateStorageId(connection
, recipientId
);
1132 private RecipientAddress
resolveRecipientAddress(
1133 final Connection connection
,
1134 final RecipientId recipientId
1135 ) throws SQLException
{
1138 SELECT r.number, r.aci, r.pni, r.username
1142 ).formatted(TABLE_RECIPIENT
);
1143 try (final var statement
= connection
.prepareStatement(sql
)) {
1144 statement
.setLong(1, recipientId
.id());
1145 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
1149 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1150 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1151 try (final var connection
= database
.getConnection()) {
1152 connection
.setAutoCommit(false);
1153 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1154 connection
.commit();
1155 } catch (SQLException e
) {
1156 throw new RuntimeException("Failed update recipient store", e
);
1159 if (!pair
.second().isEmpty()) {
1160 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1161 try (final var connection
= database
.getConnection()) {
1162 connection
.setAutoCommit(false);
1163 mergeRecipients(connection
, pair
.first(), pair
.second());
1164 connection
.commit();
1165 } catch (SQLException e
) {
1166 throw new RuntimeException("Failed update recipient store", e
);
1169 return pair
.first();
1172 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1173 final Connection connection
,
1174 final RecipientAddress address
,
1175 final boolean isSelf
1176 ) throws SQLException
{
1177 if (address
.hasSingleIdentifier() || (
1178 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1180 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1182 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1183 markRegistered(connection
, pair
.first());
1185 for (final var toBeMergedRecipientId
: pair
.second()) {
1186 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1192 private void mergeRecipients(
1193 final Connection connection
,
1194 final RecipientId recipientId
,
1195 final List
<RecipientId
> toBeMergedRecipientIds
1196 ) throws SQLException
{
1197 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1198 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1199 deleteRecipient(connection
, toBeMergedRecipientId
);
1200 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1204 private RecipientId
resolveRecipientLocked(Connection connection
, RecipientAddress address
) throws SQLException
{
1205 final var byAci
= address
.aci().isEmpty()
1206 ? Optional
.<RecipientWithAddress
>empty()
1207 : findByServiceId(connection
, address
.aci().get());
1209 if (byAci
.isPresent()) {
1210 return byAci
.get().id();
1213 final var byPni
= address
.pni().isEmpty()
1214 ? Optional
.<RecipientWithAddress
>empty()
1215 : findByServiceId(connection
, address
.pni().get());
1217 if (byPni
.isPresent()) {
1218 return byPni
.get().id();
1221 final var byNumber
= address
.number().isEmpty()
1222 ? Optional
.<RecipientWithAddress
>empty()
1223 : findByNumber(connection
, address
.number().get());
1225 if (byNumber
.isPresent()) {
1226 return byNumber
.get().id();
1229 logger
.debug("Got new recipient, both serviceId and number are unknown");
1231 if (address
.serviceId().isEmpty()) {
1232 return addNewRecipient(connection
, address
);
1235 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1238 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1239 final var recipient
= findByServiceId(connection
, serviceId
);
1241 if (recipient
.isEmpty()) {
1242 logger
.debug("Got new recipient, serviceId is unknown");
1243 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1246 return recipient
.get().id();
1249 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1250 final var recipient
= findByNumber(connection
, number
);
1252 if (recipient
.isEmpty()) {
1253 logger
.debug("Got new recipient, number is unknown");
1254 return addNewRecipient(connection
, new RecipientAddress(number
));
1257 return recipient
.get().id();
1260 private RecipientId
addNewRecipient(
1261 final Connection connection
,
1262 final RecipientAddress address
1263 ) throws SQLException
{
1266 INSERT INTO %s (number, aci, pni, username)
1270 ).formatted(TABLE_RECIPIENT
);
1271 try (final var statement
= connection
.prepareStatement(sql
)) {
1272 statement
.setString(1, address
.number().orElse(null));
1273 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1274 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1275 statement
.setString(4, address
.username().orElse(null));
1276 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1277 if (generatedKey
.isPresent()) {
1278 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1279 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1282 throw new RuntimeException("Failed to add new recipient to database");
1287 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1288 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1292 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1295 ).formatted(TABLE_RECIPIENT
);
1296 try (final var statement
= connection
.prepareStatement(sql
)) {
1297 statement
.setLong(1, recipientId
.id());
1298 statement
.executeUpdate();
1302 private void updateRecipientAddress(
1303 Connection connection
,
1304 RecipientId recipientId
,
1305 final RecipientAddress address
1306 ) throws SQLException
{
1307 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1311 SET number = ?, aci = ?, pni = ?, username = ?
1314 ).formatted(TABLE_RECIPIENT
);
1315 try (final var statement
= connection
.prepareStatement(sql
)) {
1316 statement
.setString(1, address
.number().orElse(null));
1317 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1318 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1319 statement
.setString(4, address
.username().orElse(null));
1320 statement
.setLong(5, recipientId
.id());
1321 statement
.executeUpdate();
1323 rotateStorageId(connection
, recipientId
);
1326 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1332 ).formatted(TABLE_RECIPIENT
);
1333 try (final var statement
= connection
.prepareStatement(sql
)) {
1334 statement
.setLong(1, recipientId
.id());
1335 statement
.executeUpdate();
1339 private void mergeRecipientsLocked(
1340 Connection connection
,
1341 RecipientId recipientId
,
1342 RecipientId toBeMergedRecipientId
1343 ) throws SQLException
{
1344 final var contact
= getContact(connection
, recipientId
);
1345 if (contact
== null) {
1346 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1347 storeContact(connection
, recipientId
, toBeMergedContact
);
1350 final var profileKey
= getProfileKey(connection
, recipientId
);
1351 if (profileKey
== null) {
1352 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1353 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1356 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1357 if (profileKeyCredential
== null) {
1358 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1359 toBeMergedRecipientId
);
1360 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1363 final var profile
= getProfile(connection
, recipientId
);
1364 if (profile
== null) {
1365 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1366 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1369 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1372 private Optional
<RecipientWithAddress
> findByNumber(
1373 final Connection connection
,
1375 ) throws SQLException
{
1377 SELECT r._id, r.number, r.aci, r.pni, r.username
1381 """.formatted(TABLE_RECIPIENT
);
1382 try (final var statement
= connection
.prepareStatement(sql
)) {
1383 statement
.setString(1, number
);
1384 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1388 private Optional
<RecipientWithAddress
> findByUsername(
1389 final Connection connection
,
1390 final String username
1391 ) throws SQLException
{
1393 SELECT r._id, r.number, r.aci, r.pni, r.username
1395 WHERE r.username = ?
1397 """.formatted(TABLE_RECIPIENT
);
1398 try (final var statement
= connection
.prepareStatement(sql
)) {
1399 statement
.setString(1, username
);
1400 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1404 private Optional
<RecipientWithAddress
> findByServiceId(
1405 final Connection connection
,
1406 final ServiceId serviceId
1407 ) throws SQLException
{
1408 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1409 if (recipientWithAddress
.isPresent()) {
1410 return recipientWithAddress
;
1413 SELECT r._id, r.number, r.aci, r.pni, r.username
1417 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1418 try (final var statement
= connection
.prepareStatement(sql
)) {
1419 statement
.setString(1, serviceId
.toString());
1420 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1421 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1422 return recipientWithAddress
;
1426 private Set
<RecipientWithAddress
> findAllByAddress(
1427 final Connection connection
,
1428 final RecipientAddress address
1429 ) throws SQLException
{
1431 SELECT r._id, r.number, r.aci, r.pni, r.username
1437 """.formatted(TABLE_RECIPIENT
);
1438 try (final var statement
= connection
.prepareStatement(sql
)) {
1439 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1440 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1441 statement
.setString(3, address
.number().orElse(null));
1442 statement
.setString(4, address
.username().orElse(null));
1443 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1444 .collect(Collectors
.toSet());
1448 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1451 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
1453 WHERE r._id = ? AND (%s)
1455 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1456 try (final var statement
= connection
.prepareStatement(sql
)) {
1457 statement
.setLong(1, recipientId
.id());
1458 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1462 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1463 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1464 if (recipientId
.equals(selfRecipientId
)) {
1465 return selfProfileKeyProvider
.getSelfProfileKey();
1469 SELECT r.profile_key
1473 ).formatted(TABLE_RECIPIENT
);
1474 try (final var statement
= connection
.prepareStatement(sql
)) {
1475 statement
.setLong(1, recipientId
.id());
1476 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1480 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1481 final Connection connection
,
1482 final RecipientId recipientId
1483 ) throws SQLException
{
1486 SELECT r.profile_key_credential
1490 ).formatted(TABLE_RECIPIENT
);
1491 try (final var statement
= connection
.prepareStatement(sql
)) {
1492 statement
.setLong(1, recipientId
.id());
1493 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1498 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1501 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
1503 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1505 ).formatted(TABLE_RECIPIENT
);
1506 try (final var statement
= connection
.prepareStatement(sql
)) {
1507 statement
.setLong(1, recipientId
.id());
1508 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1512 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1513 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrNull
);
1514 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrNull
);
1515 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1516 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1517 return new RecipientAddress(aci
, pni
, number
, username
);
1520 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1521 return new RecipientId(resultSet
.getLong("_id"), this);
1524 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1525 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1526 getRecipientAddressFromResultSet(resultSet
));
1529 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1530 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1531 getRecipientAddressFromResultSet(resultSet
),
1532 getContactFromResultSet(resultSet
),
1533 getProfileKeyFromResultSet(resultSet
),
1534 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1535 getProfileFromResultSet(resultSet
),
1536 getDiscoverableFromResultSet(resultSet
),
1537 getStorageRecordFromResultSet(resultSet
));
1540 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1541 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1542 return new Contact(resultSet
.getString("given_name"),
1543 resultSet
.getString("family_name"),
1544 resultSet
.getString("nick_name"),
1545 resultSet
.getString("nick_name_given_name"),
1546 resultSet
.getString("nick_name_family_name"),
1547 resultSet
.getString("note"),
1548 resultSet
.getString("color"),
1549 resultSet
.getInt("expiration_time"),
1550 resultSet
.getInt("expiration_time_version"),
1551 resultSet
.getLong("mute_until"),
1552 resultSet
.getBoolean("hide_story"),
1553 resultSet
.getBoolean("blocked"),
1554 resultSet
.getBoolean("archived"),
1555 resultSet
.getBoolean("profile_sharing"),
1556 resultSet
.getBoolean("hidden"),
1557 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1560 private static Boolean
getDiscoverableFromResultSet(final ResultSet resultSet
) throws SQLException
{
1561 final var discoverable
= resultSet
.getBoolean("discoverable");
1562 if (resultSet
.wasNull()) {
1565 return discoverable
;
1568 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1569 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1570 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1571 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1572 resultSet
.getString("profile_given_name"),
1573 resultSet
.getString("profile_family_name"),
1574 resultSet
.getString("profile_about"),
1575 resultSet
.getString("profile_about_emoji"),
1576 resultSet
.getString("profile_avatar_url_path"),
1577 resultSet
.getBytes("profile_mobile_coin_address"),
1578 profileUnidentifiedAccessMode
== null
1579 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1580 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1581 profileCapabilities
== null
1583 : Arrays
.stream(profileCapabilities
.split(","))
1584 .map(Profile
.Capability
::valueOfOrNull
)
1585 .filter(Objects
::nonNull
)
1586 .collect(Collectors
.toSet()),
1587 PhoneNumberSharingMode
.valueOfOrNull(resultSet
.getString("profile_phone_number_sharing")));
1590 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1591 final var profileKey
= resultSet
.getBytes("profile_key");
1593 if (profileKey
== null) {
1597 return new ProfileKey(profileKey
);
1598 } catch (InvalidInputException ignored
) {
1603 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1604 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1606 if (profileKeyCredential
== null) {
1610 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1611 } catch (Throwable ignored
) {
1616 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1617 final var storageId
= resultSet
.getBytes("storage_id");
1618 return StorageId
.forContact(storageId
);
1621 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1622 return resultSet
.getBytes("storage_record");
1625 public interface RecipientMergeHandler
{
1627 void mergeRecipients(
1628 final Connection connection
,
1629 RecipientId recipientId
,
1630 RecipientId toBeMergedRecipientId
1631 ) throws SQLException
;
1634 private class HelperStore
implements MergeRecipientHelper
.Store
{
1636 private final Connection connection
;
1638 public HelperStore(final Connection connection
) {
1639 this.connection
= connection
;
1643 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1644 return RecipientStore
.this.findAllByAddress(connection
, address
);
1648 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1649 return RecipientStore
.this.addNewRecipient(connection
, address
);
1653 public void updateRecipientAddress(
1654 final RecipientId recipientId
,
1655 final RecipientAddress address
1656 ) throws SQLException
{
1657 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1661 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1662 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);