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.pni 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
);
395 } catch (InvalidAddress e
) {
396 try (final var statement
= connection
.prepareStatement("""
397 UPDATE %s SET aci=NULL, pni=NULL, username=NULL, number=NULL, storage_id=NULL WHERE storage_id = ?
398 """.formatted(TABLE_RECIPIENT
))) {
399 statement
.setBytes(1, storageId
.getRaw());
400 statement
.executeUpdate();
407 public List
<Recipient
> getRecipients(
408 boolean onlyContacts
,
409 Optional
<Boolean
> blocked
,
410 Set
<RecipientId
> recipientIds
,
411 Optional
<String
> name
413 final var sqlWhere
= new ArrayList
<String
>();
415 sqlWhere
.add("r.unregistered_timestamp IS NULL");
416 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
417 sqlWhere
.add("r.hidden = FALSE");
419 if (blocked
.isPresent()) {
420 sqlWhere
.add("r.blocked = ?");
422 if (!recipientIds
.isEmpty()) {
423 final var recipientIdsCommaSeparated
= recipientIds
.stream()
424 .map(recipientId
-> String
.valueOf(recipientId
.id()))
425 .collect(Collectors
.joining(","));
426 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
431 r.number, r.aci, r.pni, r.username,
432 r.profile_key, r.profile_key_credential,
433 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,
434 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,
438 WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s
440 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
441 final var selfAddress
= selfAddressProvider
.getSelfAddress();
442 try (final var connection
= database
.getConnection()) {
443 try (final var statement
= connection
.prepareStatement(sql
)) {
444 if (blocked
.isPresent()) {
445 statement
.setBoolean(1, blocked
.get());
447 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
448 return result
.filter(r
-> name
.isEmpty() || (
449 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
450 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
451 if (r
.getAddress().matches(selfAddress
)) {
452 return Recipient
.newBuilder(r
)
453 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
460 } catch (SQLException e
) {
461 throw new RuntimeException("Failed read from recipient store", e
);
465 public Set
<String
> getAllNumbers() {
470 WHERE r.number IS NOT NULL
472 ).formatted(TABLE_RECIPIENT
);
473 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
474 try (final var connection
= database
.getConnection()) {
475 try (final var statement
= connection
.prepareStatement(sql
)) {
476 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
477 .filter(Objects
::nonNull
)
478 .filter(n
-> !n
.equals(selfNumber
))
483 } catch (NumberFormatException e
) {
487 .collect(Collectors
.toSet());
489 } catch (SQLException e
) {
490 throw new RuntimeException("Failed read from recipient store", e
);
494 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
497 SELECT r.aci, r.profile_key
499 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
501 ).formatted(TABLE_RECIPIENT
);
502 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
503 try (final var connection
= database
.getConnection()) {
504 try (final var statement
= connection
.prepareStatement(sql
)) {
505 return Utils
.executeQueryForStream(statement
, resultSet
-> {
506 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
507 if (aci
.equals(selfAci
)) {
508 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
510 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
511 return new Pair
<>(aci
, profileKey
);
512 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
514 } catch (SQLException e
) {
515 throw new RuntimeException("Failed read from recipient store", e
);
519 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
524 WHERE (r.aci IS NOT NULL OR r.pni IS NOT NULL)
526 ).formatted(TABLE_RECIPIENT
);
527 try (final var statement
= connection
.prepareStatement(sql
)) {
528 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
532 public void setMissingStorageIds() {
533 final var selectSql
= (
537 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL AND (r.aci IS NOT NULL OR r.pni IS NOT NULL)
539 ).formatted(TABLE_RECIPIENT
);
540 final var updateSql
= (
546 ).formatted(TABLE_RECIPIENT
);
547 try (final var connection
= database
.getConnection()) {
548 connection
.setAutoCommit(false);
549 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
550 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
552 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
553 for (final var recipientId
: recipientIds
) {
554 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
555 updateStmt
.setLong(2, recipientId
.id());
556 updateStmt
.executeUpdate();
561 } catch (SQLException e
) {
562 throw new RuntimeException("Failed update recipient store", e
);
567 public void deleteContact(RecipientId recipientId
) {
568 storeContact(recipientId
, null);
571 public void deleteRecipientData(RecipientId recipientId
) {
572 logger
.debug("Deleting recipient data for {}", recipientId
);
573 try (final var connection
= database
.getConnection()) {
574 connection
.setAutoCommit(false);
575 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
576 storeContact(connection
, recipientId
, null);
577 storeProfile(connection
, recipientId
, null);
578 storeProfileKey(connection
, recipientId
, null, false);
579 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
580 deleteRecipient(connection
, recipientId
);
582 } catch (SQLException e
) {
583 throw new RuntimeException("Failed update recipient store", e
);
588 public Profile
getProfile(final RecipientId recipientId
) {
589 try (final var connection
= database
.getConnection()) {
590 return getProfile(connection
, recipientId
);
591 } catch (SQLException e
) {
592 throw new RuntimeException("Failed read from recipient store", e
);
597 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
598 try (final var connection
= database
.getConnection()) {
599 return getProfileKey(connection
, recipientId
);
600 } catch (SQLException e
) {
601 throw new RuntimeException("Failed read from recipient store", e
);
606 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
607 try (final var connection
= database
.getConnection()) {
608 return getExpiringProfileKeyCredential(connection
, recipientId
);
609 } catch (SQLException e
) {
610 throw new RuntimeException("Failed read from recipient store", e
);
615 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
616 try (final var connection
= database
.getConnection()) {
617 storeProfile(connection
, recipientId
, profile
);
618 } catch (SQLException e
) {
619 throw new RuntimeException("Failed update recipient store", e
);
624 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
625 try (final var connection
= database
.getConnection()) {
626 storeProfileKey(connection
, recipientId
, profileKey
);
627 } catch (SQLException e
) {
628 throw new RuntimeException("Failed update recipient store", e
);
632 public void storeProfileKey(
633 Connection connection
,
634 RecipientId recipientId
,
635 final ProfileKey profileKey
636 ) throws SQLException
{
637 storeProfileKey(connection
, recipientId
, profileKey
, true);
641 public void storeExpiringProfileKeyCredential(
642 RecipientId recipientId
,
643 final ExpiringProfileKeyCredential profileKeyCredential
645 try (final var connection
= database
.getConnection()) {
646 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
647 } catch (SQLException e
) {
648 throw new RuntimeException("Failed update recipient store", e
);
652 public void rotateSelfStorageId() {
653 try (final var connection
= database
.getConnection()) {
654 rotateSelfStorageId(connection
);
655 } catch (SQLException e
) {
656 throw new RuntimeException("Failed update recipient store", e
);
660 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
661 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
662 rotateStorageId(connection
, selfRecipientId
);
665 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
666 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
667 return rotateStorageId(connection
, selfRecipientId
);
670 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
673 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)
674 """.formatted(TABLE_RECIPIENT
);
675 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
676 try (final var statement
= connection
.prepareStatement(sql
)) {
677 statement
.setLong(1, selfRecipientId
.id());
678 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
682 public void updateStorageId(
683 Connection connection
,
684 RecipientId recipientId
,
686 ) throws SQLException
{
693 ).formatted(TABLE_RECIPIENT
);
694 try (final var statement
= connection
.prepareStatement(sql
)) {
695 statement
.setBytes(1, storageId
.getRaw());
696 statement
.setLong(2, recipientId
.id());
697 statement
.executeUpdate();
701 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
708 ).formatted(TABLE_RECIPIENT
);
709 try (final var statement
= connection
.prepareStatement(sql
)) {
710 for (final var entry
: storageIdMap
.entrySet()) {
711 statement
.setBytes(1, entry
.getValue().getRaw());
712 statement
.setLong(2, entry
.getKey().id());
713 statement
.executeUpdate();
718 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
719 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
720 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
723 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
726 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
727 """.formatted(TABLE_RECIPIENT
);
728 try (final var statement
= connection
.prepareStatement(sql
)) {
729 statement
.setLong(1, recipientId
.id());
730 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
731 if (storageId
.isPresent()) {
732 return storageId
.get();
735 return rotateStorageId(connection
, recipientId
);
738 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
739 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
740 updateStorageId(connection
, recipientId
, newStorageId
);
744 public void storeStorageRecord(
745 final Connection connection
,
746 final RecipientId recipientId
,
747 final StorageId storageId
,
748 final byte[] storageRecord
749 ) throws SQLException
{
750 final var deleteSql
= (
753 SET storage_id = NULL
756 ).formatted(TABLE_RECIPIENT
);
757 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
758 statement
.setBytes(1, storageId
.getRaw());
759 statement
.executeUpdate();
761 final var insertSql
= (
764 SET storage_id = ?, storage_record = ?
767 ).formatted(TABLE_RECIPIENT
);
768 try (final var statement
= connection
.prepareStatement(insertSql
)) {
769 statement
.setBytes(1, storageId
.getRaw());
770 if (storageRecord
== null) {
771 statement
.setNull(2, Types
.BLOB
);
773 statement
.setBytes(2, storageRecord
);
775 statement
.setLong(3, recipientId
.id());
776 statement
.executeUpdate();
780 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
781 logger
.debug("Migrating legacy recipients to database");
782 long start
= System
.nanoTime();
785 INSERT INTO %s (_id, number, aci)
788 ).formatted(TABLE_RECIPIENT
);
789 try (final var connection
= database
.getConnection()) {
790 connection
.setAutoCommit(false);
791 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
792 statement
.executeUpdate();
794 try (final var statement
= connection
.prepareStatement(sql
)) {
795 for (final var recipient
: recipients
.values()) {
796 statement
.setLong(1, recipient
.getRecipientId().id());
797 statement
.setString(2, recipient
.getAddress().number().orElse(null));
798 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
799 statement
.executeUpdate();
802 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
804 for (final var recipient
: recipients
.values()) {
805 if (recipient
.getContact() != null) {
806 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
808 if (recipient
.getProfile() != null) {
809 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
811 if (recipient
.getProfileKey() != null) {
812 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
814 if (recipient
.getExpiringProfileKeyCredential() != null) {
815 storeExpiringProfileKeyCredential(connection
,
816 recipient
.getRecipientId(),
817 recipient
.getExpiringProfileKeyCredential());
821 } catch (SQLException e
) {
822 throw new RuntimeException("Failed update recipient store", e
);
824 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
827 long getActualRecipientId(long recipientId
) {
828 while (recipientsMerged
.containsKey(recipientId
)) {
829 final var newRecipientId
= recipientsMerged
.get(recipientId
);
830 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
831 recipientId
= newRecipientId
;
836 public void storeContact(
837 final Connection connection
,
838 final RecipientId recipientId
,
839 final Contact contact
840 ) throws SQLException
{
844 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 = ?
847 ).formatted(TABLE_RECIPIENT
);
848 try (final var statement
= connection
.prepareStatement(sql
)) {
849 statement
.setString(1, contact
== null ?
null : contact
.givenName());
850 statement
.setString(2, contact
== null ?
null : contact
.familyName());
851 statement
.setString(3, contact
== null ?
null : contact
.nickName());
852 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
853 statement
.setInt(5, contact
== null ?
0 : Math
.max(1, contact
.messageExpirationTimeVersion()));
854 statement
.setLong(6, contact
== null ?
0 : contact
.muteUntil());
855 statement
.setBoolean(7, contact
!= null && contact
.hideStory());
856 statement
.setBoolean(8, contact
!= null && contact
.isProfileSharingEnabled());
857 statement
.setString(9, contact
== null ?
null : contact
.color());
858 statement
.setBoolean(10, contact
!= null && contact
.isBlocked());
859 statement
.setBoolean(11, contact
!= null && contact
.isArchived());
860 if (contact
== null || contact
.unregisteredTimestamp() == null) {
861 statement
.setNull(12, Types
.INTEGER
);
863 statement
.setLong(12, contact
.unregisteredTimestamp());
865 statement
.setString(13, contact
== null ?
null : contact
.nickNameGivenName());
866 statement
.setString(14, contact
== null ?
null : contact
.nickNameFamilyName());
867 statement
.setString(15, contact
== null ?
null : contact
.note());
868 statement
.setLong(16, recipientId
.id());
869 statement
.executeUpdate();
871 if (contact
!= null && contact
.unregisteredTimestamp() != null) {
872 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
874 rotateStorageId(connection
, recipientId
);
877 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
878 final Connection connection
,
879 final List
<StorageId
> storageIds
880 ) throws SQLException
{
884 SET storage_id = NULL
885 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
887 ).formatted(TABLE_RECIPIENT
);
889 try (final var statement
= connection
.prepareStatement(sql
)) {
890 for (final var storageId
: storageIds
) {
891 statement
.setBytes(1, storageId
.getRaw());
892 count
+= statement
.executeUpdate();
898 public void markNeedsPniSignature(final RecipientId recipientId
, final boolean value
) {
899 logger
.debug("Marking {} numbers as need pni signature = {}", recipientId
, value
);
900 try (final var connection
= database
.getConnection()) {
904 SET needs_pni_signature = ?
907 ).formatted(TABLE_RECIPIENT
);
908 try (final var statement
= connection
.prepareStatement(sql
)) {
909 statement
.setBoolean(1, value
);
910 statement
.setLong(2, recipientId
.id());
911 statement
.executeUpdate();
913 } catch (SQLException e
) {
914 throw new RuntimeException("Failed update recipient store", e
);
918 public boolean needsPniSignature(final RecipientId recipientId
) {
919 try (final var connection
= database
.getConnection()) {
922 SELECT needs_pni_signature
926 ).formatted(TABLE_RECIPIENT
);
927 try (final var statement
= connection
.prepareStatement(sql
)) {
928 statement
.setLong(1, recipientId
.id());
929 return Utils
.executeQuerySingleRow(statement
, resultSet
-> resultSet
.getBoolean("needs_pni_signature"));
931 } catch (SQLException e
) {
932 throw new RuntimeException("Failed read recipient store", e
);
936 public void markUndiscoverablePossiblyUnregistered(final Set
<String
> numbers
) {
937 logger
.debug("Marking {} numbers as undiscoverable", numbers
.size());
938 try (final var connection
= database
.getConnection()) {
939 connection
.setAutoCommit(false);
940 for (final var number
: numbers
) {
941 final var recipientAddress
= findByNumber(connection
, number
);
942 if (recipientAddress
.isPresent()) {
943 final var recipientId
= recipientAddress
.get().id();
944 markDiscoverable(connection
, recipientId
, false);
945 final var contact
= getContact(connection
, recipientId
);
946 if (recipientAddress
.get().address().aci().isEmpty() || (
947 contact
!= null && contact
.unregisteredTimestamp() != null
949 markUnregisteredAndSplitIfNecessary(connection
, recipientId
);
954 } catch (SQLException e
) {
955 throw new RuntimeException("Failed update recipient store", e
);
959 public void markDiscoverable(final Set
<String
> numbers
) {
960 logger
.debug("Marking {} numbers as discoverable", numbers
.size());
961 try (final var connection
= database
.getConnection()) {
962 connection
.setAutoCommit(false);
963 for (final var number
: numbers
) {
964 final var recipientAddress
= findByNumber(connection
, number
);
965 if (recipientAddress
.isPresent()) {
966 final var recipientId
= recipientAddress
.get().id();
967 markDiscoverable(connection
, recipientId
, true);
971 } catch (SQLException e
) {
972 throw new RuntimeException("Failed update recipient store", e
);
976 public void markRegistered(final RecipientId recipientId
, final boolean registered
) {
977 logger
.debug("Marking {} as registered={}", recipientId
, registered
);
978 try (final var connection
= database
.getConnection()) {
979 connection
.setAutoCommit(false);
981 markRegistered(connection
, recipientId
);
983 markUnregistered(connection
, recipientId
);
986 } catch (SQLException e
) {
987 throw new RuntimeException("Failed update recipient store", e
);
991 private void markUnregisteredAndSplitIfNecessary(
992 final Connection connection
,
993 final RecipientId recipientId
994 ) throws SQLException
{
995 markUnregistered(connection
, recipientId
);
996 final var address
= resolveRecipientAddress(connection
, recipientId
);
997 if (address
.aci().isPresent() && address
.pni().isPresent()) {
998 final var numberAddress
= new RecipientAddress(address
.pni().get(), address
.number().orElse(null));
999 updateRecipientAddress(connection
, recipientId
, address
.removeIdentifiersFrom(numberAddress
));
1000 addNewRecipient(connection
, numberAddress
);
1004 private void markDiscoverable(
1005 final Connection connection
,
1006 final RecipientId recipientId
,
1007 final boolean discoverable
1008 ) throws SQLException
{
1012 SET discoverable = ?
1015 ).formatted(TABLE_RECIPIENT
);
1016 try (final var statement
= connection
.prepareStatement(sql
)) {
1017 statement
.setBoolean(1, discoverable
);
1018 statement
.setLong(2, recipientId
.id());
1019 statement
.executeUpdate();
1023 private void markRegistered(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1027 SET unregistered_timestamp = NULL
1030 ).formatted(TABLE_RECIPIENT
);
1031 try (final var statement
= connection
.prepareStatement(sql
)) {
1032 statement
.setLong(1, recipientId
.id());
1033 statement
.executeUpdate();
1037 private void markUnregistered(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1041 SET unregistered_timestamp = ?, discoverable = FALSE
1044 ).formatted(TABLE_RECIPIENT
);
1045 try (final var statement
= connection
.prepareStatement(sql
)) {
1046 statement
.setLong(1, System
.currentTimeMillis());
1047 statement
.setLong(2, recipientId
.id());
1048 statement
.executeUpdate();
1052 private void storeExpiringProfileKeyCredential(
1053 final Connection connection
,
1054 final RecipientId recipientId
,
1055 final ExpiringProfileKeyCredential profileKeyCredential
1056 ) throws SQLException
{
1060 SET profile_key_credential = ?
1063 ).formatted(TABLE_RECIPIENT
);
1064 try (final var statement
= connection
.prepareStatement(sql
)) {
1065 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
1066 statement
.setLong(2, recipientId
.id());
1067 statement
.executeUpdate();
1071 public void storeProfile(
1072 final Connection connection
,
1073 final RecipientId recipientId
,
1074 final Profile profile
1075 ) throws SQLException
{
1079 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 = ?
1082 ).formatted(TABLE_RECIPIENT
);
1083 try (final var statement
= connection
.prepareStatement(sql
)) {
1084 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
1085 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
1086 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
1087 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
1088 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
1089 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
1090 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
1091 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
1092 statement
.setString(9,
1095 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
1096 statement
.setString(10,
1097 profile
== null || profile
.getPhoneNumberSharingMode() == null
1099 : profile
.getPhoneNumberSharingMode().name());
1100 statement
.setLong(11, recipientId
.id());
1101 statement
.executeUpdate();
1103 rotateStorageId(connection
, recipientId
);
1106 private void storeProfileKey(
1107 Connection connection
,
1108 RecipientId recipientId
,
1109 final ProfileKey profileKey
,
1110 boolean resetProfile
1111 ) throws SQLException
{
1112 if (profileKey
!= null) {
1113 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
1114 if (profileKey
.equals(recipientProfileKey
)) {
1115 final var recipientProfile
= getProfile(connection
, recipientId
);
1116 if (recipientProfile
== null || (
1117 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
1118 && recipientProfile
.getUnidentifiedAccessMode()
1119 != Profile
.UnidentifiedAccessMode
.DISABLED
1129 SET profile_key = ?, profile_key_credential = NULL%s
1132 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
1133 try (final var statement
= connection
.prepareStatement(sql
)) {
1134 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
1135 statement
.setLong(2, recipientId
.id());
1136 statement
.executeUpdate();
1138 rotateStorageId(connection
, recipientId
);
1141 private RecipientAddress
resolveRecipientAddress(
1142 final Connection connection
,
1143 final RecipientId recipientId
1144 ) throws SQLException
{
1147 SELECT r.number, r.aci, r.pni, r.username
1151 ).formatted(TABLE_RECIPIENT
);
1152 try (final var statement
= connection
.prepareStatement(sql
)) {
1153 statement
.setLong(1, recipientId
.id());
1154 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
1158 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1159 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1160 try (final var connection
= database
.getConnection()) {
1161 connection
.setAutoCommit(false);
1162 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1163 connection
.commit();
1164 } catch (SQLException e
) {
1165 throw new RuntimeException("Failed update recipient store", e
);
1168 if (!pair
.second().isEmpty()) {
1169 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1170 try (final var connection
= database
.getConnection()) {
1171 connection
.setAutoCommit(false);
1172 mergeRecipients(connection
, pair
.first(), pair
.second());
1173 connection
.commit();
1174 } catch (SQLException e
) {
1175 throw new RuntimeException("Failed update recipient store", e
);
1178 return pair
.first();
1181 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1182 final Connection connection
,
1183 final RecipientAddress address
,
1184 final boolean isSelf
1185 ) throws SQLException
{
1186 if (address
.hasSingleIdentifier() || (
1187 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1189 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1191 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1192 markRegistered(connection
, pair
.first());
1194 for (final var toBeMergedRecipientId
: pair
.second()) {
1195 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1201 private void mergeRecipients(
1202 final Connection connection
,
1203 final RecipientId recipientId
,
1204 final List
<RecipientId
> toBeMergedRecipientIds
1205 ) throws SQLException
{
1206 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1207 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1208 deleteRecipient(connection
, toBeMergedRecipientId
);
1209 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1213 private RecipientId
resolveRecipientLocked(Connection connection
, RecipientAddress address
) throws SQLException
{
1214 final var byAci
= address
.aci().isEmpty()
1215 ? Optional
.<RecipientWithAddress
>empty()
1216 : findByServiceId(connection
, address
.aci().get());
1218 if (byAci
.isPresent()) {
1219 return byAci
.get().id();
1222 final var byPni
= address
.pni().isEmpty()
1223 ? Optional
.<RecipientWithAddress
>empty()
1224 : findByServiceId(connection
, address
.pni().get());
1226 if (byPni
.isPresent()) {
1227 return byPni
.get().id();
1230 final var byNumber
= address
.number().isEmpty()
1231 ? Optional
.<RecipientWithAddress
>empty()
1232 : findByNumber(connection
, address
.number().get());
1234 if (byNumber
.isPresent()) {
1235 return byNumber
.get().id();
1238 logger
.debug("Got new recipient, both serviceId and number are unknown");
1240 if (address
.serviceId().isEmpty()) {
1241 return addNewRecipient(connection
, address
);
1244 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1247 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1248 final var recipient
= findByServiceId(connection
, serviceId
);
1250 if (recipient
.isEmpty()) {
1251 logger
.debug("Got new recipient, serviceId is unknown");
1252 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1255 return recipient
.get().id();
1258 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1259 final var recipient
= findByNumber(connection
, number
);
1261 if (recipient
.isEmpty()) {
1262 logger
.debug("Got new recipient, number is unknown");
1263 return addNewRecipient(connection
, new RecipientAddress(number
));
1266 return recipient
.get().id();
1269 private RecipientId
addNewRecipient(
1270 final Connection connection
,
1271 final RecipientAddress address
1272 ) throws SQLException
{
1275 INSERT INTO %s (number, aci, pni, username)
1279 ).formatted(TABLE_RECIPIENT
);
1280 try (final var statement
= connection
.prepareStatement(sql
)) {
1281 statement
.setString(1, address
.number().orElse(null));
1282 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1283 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1284 statement
.setString(4, address
.username().orElse(null));
1285 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1286 if (generatedKey
.isPresent()) {
1287 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1288 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1291 throw new RuntimeException("Failed to add new recipient to database");
1296 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1297 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1301 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1304 ).formatted(TABLE_RECIPIENT
);
1305 try (final var statement
= connection
.prepareStatement(sql
)) {
1306 statement
.setLong(1, recipientId
.id());
1307 statement
.executeUpdate();
1311 private void updateRecipientAddress(
1312 Connection connection
,
1313 RecipientId recipientId
,
1314 final RecipientAddress address
1315 ) throws SQLException
{
1316 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1320 SET number = ?, aci = ?, pni = ?, username = ?
1323 ).formatted(TABLE_RECIPIENT
);
1324 try (final var statement
= connection
.prepareStatement(sql
)) {
1325 statement
.setString(1, address
.number().orElse(null));
1326 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1327 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1328 statement
.setString(4, address
.username().orElse(null));
1329 statement
.setLong(5, recipientId
.id());
1330 statement
.executeUpdate();
1332 rotateStorageId(connection
, recipientId
);
1335 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1341 ).formatted(TABLE_RECIPIENT
);
1342 try (final var statement
= connection
.prepareStatement(sql
)) {
1343 statement
.setLong(1, recipientId
.id());
1344 statement
.executeUpdate();
1348 private void mergeRecipientsLocked(
1349 Connection connection
,
1350 RecipientId recipientId
,
1351 RecipientId toBeMergedRecipientId
1352 ) throws SQLException
{
1353 final var contact
= getContact(connection
, recipientId
);
1354 if (contact
== null) {
1355 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1356 storeContact(connection
, recipientId
, toBeMergedContact
);
1359 final var profileKey
= getProfileKey(connection
, recipientId
);
1360 if (profileKey
== null) {
1361 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1362 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1365 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1366 if (profileKeyCredential
== null) {
1367 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1368 toBeMergedRecipientId
);
1369 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1372 final var profile
= getProfile(connection
, recipientId
);
1373 if (profile
== null) {
1374 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1375 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1378 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1381 private Optional
<RecipientWithAddress
> findByNumber(
1382 final Connection connection
,
1384 ) throws SQLException
{
1386 SELECT r._id, r.number, r.aci, r.pni, r.username
1390 """.formatted(TABLE_RECIPIENT
);
1391 try (final var statement
= connection
.prepareStatement(sql
)) {
1392 statement
.setString(1, number
);
1393 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1397 private Optional
<RecipientWithAddress
> findByUsername(
1398 final Connection connection
,
1399 final String username
1400 ) throws SQLException
{
1402 SELECT r._id, r.number, r.aci, r.pni, r.username
1404 WHERE r.username = ?
1406 """.formatted(TABLE_RECIPIENT
);
1407 try (final var statement
= connection
.prepareStatement(sql
)) {
1408 statement
.setString(1, username
);
1409 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1413 private Optional
<RecipientWithAddress
> findByServiceId(
1414 final Connection connection
,
1415 final ServiceId serviceId
1416 ) throws SQLException
{
1417 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1418 if (recipientWithAddress
.isPresent()) {
1419 return recipientWithAddress
;
1422 SELECT r._id, r.number, r.aci, r.pni, r.username
1426 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1427 try (final var statement
= connection
.prepareStatement(sql
)) {
1428 statement
.setString(1, serviceId
.toString());
1429 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1430 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1431 return recipientWithAddress
;
1435 private Set
<RecipientWithAddress
> findAllByAddress(
1436 final Connection connection
,
1437 final RecipientAddress address
1438 ) throws SQLException
{
1440 SELECT r._id, r.number, r.aci, r.pni, r.username
1446 """.formatted(TABLE_RECIPIENT
);
1447 try (final var statement
= connection
.prepareStatement(sql
)) {
1448 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1449 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1450 statement
.setString(3, address
.number().orElse(null));
1451 statement
.setString(4, address
.username().orElse(null));
1452 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1453 .collect(Collectors
.toSet());
1457 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1460 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
1462 WHERE r._id = ? AND (%s)
1464 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1465 try (final var statement
= connection
.prepareStatement(sql
)) {
1466 statement
.setLong(1, recipientId
.id());
1467 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1471 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1472 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1473 if (recipientId
.equals(selfRecipientId
)) {
1474 return selfProfileKeyProvider
.getSelfProfileKey();
1478 SELECT r.profile_key
1482 ).formatted(TABLE_RECIPIENT
);
1483 try (final var statement
= connection
.prepareStatement(sql
)) {
1484 statement
.setLong(1, recipientId
.id());
1485 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1489 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1490 final Connection connection
,
1491 final RecipientId recipientId
1492 ) throws SQLException
{
1495 SELECT r.profile_key_credential
1499 ).formatted(TABLE_RECIPIENT
);
1500 try (final var statement
= connection
.prepareStatement(sql
)) {
1501 statement
.setLong(1, recipientId
.id());
1502 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1507 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1510 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
1512 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1514 ).formatted(TABLE_RECIPIENT
);
1515 try (final var statement
= connection
.prepareStatement(sql
)) {
1516 statement
.setLong(1, recipientId
.id());
1517 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1521 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1522 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrNull
);
1523 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrNull
);
1524 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1525 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1526 return new RecipientAddress(aci
, pni
, number
, username
);
1529 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1530 return new RecipientId(resultSet
.getLong("_id"), this);
1533 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1534 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1535 getRecipientAddressFromResultSet(resultSet
));
1538 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1539 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1540 getRecipientAddressFromResultSet(resultSet
),
1541 getContactFromResultSet(resultSet
),
1542 getProfileKeyFromResultSet(resultSet
),
1543 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1544 getProfileFromResultSet(resultSet
),
1545 getDiscoverableFromResultSet(resultSet
),
1546 getStorageRecordFromResultSet(resultSet
));
1549 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1550 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1551 return new Contact(resultSet
.getString("given_name"),
1552 resultSet
.getString("family_name"),
1553 resultSet
.getString("nick_name"),
1554 resultSet
.getString("nick_name_given_name"),
1555 resultSet
.getString("nick_name_family_name"),
1556 resultSet
.getString("note"),
1557 resultSet
.getString("color"),
1558 resultSet
.getInt("expiration_time"),
1559 resultSet
.getInt("expiration_time_version"),
1560 resultSet
.getLong("mute_until"),
1561 resultSet
.getBoolean("hide_story"),
1562 resultSet
.getBoolean("blocked"),
1563 resultSet
.getBoolean("archived"),
1564 resultSet
.getBoolean("profile_sharing"),
1565 resultSet
.getBoolean("hidden"),
1566 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1569 private static Boolean
getDiscoverableFromResultSet(final ResultSet resultSet
) throws SQLException
{
1570 final var discoverable
= resultSet
.getBoolean("discoverable");
1571 if (resultSet
.wasNull()) {
1574 return discoverable
;
1577 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1578 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1579 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1580 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1581 resultSet
.getString("profile_given_name"),
1582 resultSet
.getString("profile_family_name"),
1583 resultSet
.getString("profile_about"),
1584 resultSet
.getString("profile_about_emoji"),
1585 resultSet
.getString("profile_avatar_url_path"),
1586 resultSet
.getBytes("profile_mobile_coin_address"),
1587 profileUnidentifiedAccessMode
== null
1588 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1589 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1590 profileCapabilities
== null
1592 : Arrays
.stream(profileCapabilities
.split(","))
1593 .map(Profile
.Capability
::valueOfOrNull
)
1594 .filter(Objects
::nonNull
)
1595 .collect(Collectors
.toSet()),
1596 PhoneNumberSharingMode
.valueOfOrNull(resultSet
.getString("profile_phone_number_sharing")));
1599 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1600 final var profileKey
= resultSet
.getBytes("profile_key");
1602 if (profileKey
== null) {
1606 return new ProfileKey(profileKey
);
1607 } catch (InvalidInputException ignored
) {
1612 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1613 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1615 if (profileKeyCredential
== null) {
1619 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1620 } catch (Throwable ignored
) {
1625 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1626 final var storageId
= resultSet
.getBytes("storage_id");
1627 return StorageId
.forContact(storageId
);
1630 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1631 return resultSet
.getBytes("storage_record");
1634 public interface RecipientMergeHandler
{
1636 void mergeRecipients(
1637 final Connection connection
,
1638 RecipientId recipientId
,
1639 RecipientId toBeMergedRecipientId
1640 ) throws SQLException
;
1643 private class HelperStore
implements MergeRecipientHelper
.Store
{
1645 private final Connection connection
;
1647 public HelperStore(final Connection connection
) {
1648 this.connection
= connection
;
1652 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1653 return RecipientStore
.this.findAllByAddress(connection
, address
);
1657 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1658 return RecipientStore
.this.addNewRecipient(connection
, address
);
1662 public void updateRecipientAddress(
1663 final RecipientId recipientId
,
1664 final RecipientAddress address
1665 ) throws SQLException
{
1666 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1670 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1671 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);