1 package org
.asamk
.signal
.manager
.storage
.recipients
;
3 import org
.asamk
.signal
.manager
.api
.Contact
;
4 import org
.asamk
.signal
.manager
.api
.Pair
;
5 import org
.asamk
.signal
.manager
.api
.Profile
;
6 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
7 import org
.asamk
.signal
.manager
.storage
.Database
;
8 import org
.asamk
.signal
.manager
.storage
.Utils
;
9 import org
.asamk
.signal
.manager
.storage
.contacts
.ContactsStore
;
10 import org
.asamk
.signal
.manager
.storage
.profiles
.ProfileStore
;
11 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
12 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
13 import org
.signal
.libsignal
.zkgroup
.profiles
.ExpiringProfileKeyCredential
;
14 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
15 import org
.slf4j
.Logger
;
16 import org
.slf4j
.LoggerFactory
;
17 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
18 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
19 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
20 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
21 import org
.whispersystems
.signalservice
.api
.storage
.StorageId
;
23 import java
.sql
.Connection
;
24 import java
.sql
.ResultSet
;
25 import java
.sql
.SQLException
;
26 import java
.sql
.Types
;
27 import java
.util
.ArrayList
;
28 import java
.util
.Arrays
;
29 import java
.util
.Collection
;
30 import java
.util
.HashMap
;
31 import java
.util
.List
;
33 import java
.util
.Objects
;
34 import java
.util
.Optional
;
36 import java
.util
.function
.Supplier
;
37 import java
.util
.stream
.Collectors
;
39 public class RecipientStore
implements RecipientIdCreator
, RecipientResolver
, RecipientTrustedResolver
, ContactsStore
, ProfileStore
{
41 private static final Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
42 private static final String TABLE_RECIPIENT
= "recipient";
43 private static final String SQL_IS_CONTACT
= "r.given_name IS NOT NULL OR r.family_name IS NOT NULL OR r.nick_name IS NOT NULL OR r.expiration_time > 0 OR r.profile_sharing = TRUE OR r.color IS NOT NULL OR r.blocked = TRUE OR r.archived = TRUE";
45 private final RecipientMergeHandler recipientMergeHandler
;
46 private final SelfAddressProvider selfAddressProvider
;
47 private final SelfProfileKeyProvider selfProfileKeyProvider
;
48 private final Database database
;
50 private final Object recipientsLock
= new Object();
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,
69 profile_key_credential BLOB,
76 expiration_time INTEGER NOT NULL DEFAULT 0,
77 mute_until INTEGER NOT NULL DEFAULT 0,
78 blocked INTEGER NOT NULL DEFAULT FALSE,
79 archived INTEGER NOT NULL DEFAULT FALSE,
80 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
81 hide_story INTEGER NOT NULL DEFAULT FALSE,
82 hidden INTEGER NOT NULL DEFAULT FALSE,
84 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
85 profile_given_name TEXT,
86 profile_family_name TEXT,
88 profile_about_emoji TEXT,
89 profile_avatar_url_path TEXT,
90 profile_mobile_coin_address BLOB,
91 profile_unidentified_access_mode TEXT,
92 profile_capabilities TEXT
98 public RecipientStore(
99 final RecipientMergeHandler recipientMergeHandler
,
100 final SelfAddressProvider selfAddressProvider
,
101 final SelfProfileKeyProvider selfProfileKeyProvider
,
102 final Database database
104 this.recipientMergeHandler
= recipientMergeHandler
;
105 this.selfAddressProvider
= selfAddressProvider
;
106 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
107 this.database
= database
;
110 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
113 SELECT r.number, r.aci, r.pni, r.username
117 ).formatted(TABLE_RECIPIENT
);
118 try (final var connection
= database
.getConnection()) {
119 try (final var statement
= connection
.prepareStatement(sql
)) {
120 statement
.setLong(1, recipientId
.id());
121 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
123 } catch (SQLException e
) {
124 throw new RuntimeException("Failed read from recipient store", e
);
128 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
133 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
135 ).formatted(TABLE_RECIPIENT
);
136 try (final var connection
= database
.getConnection()) {
137 try (final var statement
= connection
.prepareStatement(sql
)) {
138 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
139 return result
.toList();
142 } catch (SQLException e
) {
143 throw new RuntimeException("Failed read from recipient store", e
);
148 public RecipientId
resolveRecipient(final long rawRecipientId
) {
155 ).formatted(TABLE_RECIPIENT
);
156 try (final var connection
= database
.getConnection()) {
157 try (final var statement
= connection
.prepareStatement(sql
)) {
158 statement
.setLong(1, rawRecipientId
);
159 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
161 } catch (SQLException e
) {
162 throw new RuntimeException("Failed read from recipient store", e
);
167 public RecipientId
resolveRecipient(final String identifier
) {
168 final var serviceId
= ServiceId
.parseOrNull(identifier
);
169 if (serviceId
!= null) {
170 return resolveRecipient(serviceId
);
172 return resolveRecipientByNumber(identifier
);
176 private RecipientId
resolveRecipientByNumber(final String number
) {
177 synchronized (recipientsLock
) {
178 final RecipientId recipientId
;
179 try (final var connection
= database
.getConnection()) {
180 connection
.setAutoCommit(false);
181 recipientId
= resolveRecipientLocked(connection
, number
);
183 } catch (SQLException e
) {
184 throw new RuntimeException("Failed read recipient store", e
);
191 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
192 synchronized (recipientsLock
) {
193 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
194 if (recipientWithAddress
!= null) {
195 return recipientWithAddress
.id();
197 try (final var connection
= database
.getConnection()) {
198 connection
.setAutoCommit(false);
199 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
202 } catch (SQLException e
) {
203 throw new RuntimeException("Failed read recipient store", e
);
209 * Should only be used for recipientIds from the database.
210 * Where the foreign key relations ensure a valid recipientId.
213 public RecipientId
create(final long recipientId
) {
214 return new RecipientId(recipientId
, this);
217 public RecipientId
resolveRecipientByNumber(
218 final String number
, Supplier
<ServiceId
> serviceIdSupplier
219 ) throws UnregisteredRecipientException
{
220 final Optional
<RecipientWithAddress
> byNumber
;
221 try (final var connection
= database
.getConnection()) {
222 byNumber
= findByNumber(connection
, number
);
223 } catch (SQLException e
) {
224 throw new RuntimeException("Failed read from recipient store", e
);
226 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
227 final var serviceId
= serviceIdSupplier
.get();
228 if (serviceId
== null) {
229 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
233 return resolveRecipient(serviceId
);
235 return byNumber
.get().id();
238 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
239 final Optional
<RecipientWithAddress
> byNumber
;
240 try (final var connection
= database
.getConnection()) {
241 byNumber
= findByNumber(connection
, number
);
242 } catch (SQLException e
) {
243 throw new RuntimeException("Failed read from recipient store", e
);
245 return byNumber
.map(RecipientWithAddress
::id
);
248 public RecipientId
resolveRecipientByUsername(
249 final String username
, Supplier
<ACI
> aciSupplier
250 ) throws UnregisteredRecipientException
{
251 final Optional
<RecipientWithAddress
> byUsername
;
252 try (final var connection
= database
.getConnection()) {
253 byUsername
= findByUsername(connection
, username
);
254 } catch (SQLException e
) {
255 throw new RuntimeException("Failed read from recipient store", e
);
257 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
258 final var aci
= aciSupplier
.get();
260 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
265 return resolveRecipientTrusted(aci
, username
);
267 return byUsername
.get().id();
270 public RecipientId
resolveRecipient(RecipientAddress address
) {
271 synchronized (recipientsLock
) {
272 final RecipientId recipientId
;
273 try (final var connection
= database
.getConnection()) {
274 connection
.setAutoCommit(false);
275 recipientId
= resolveRecipientLocked(connection
, address
);
277 } catch (SQLException e
) {
278 throw new RuntimeException("Failed read recipient store", e
);
284 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
285 return resolveRecipientLocked(connection
, address
);
289 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
290 return resolveRecipientTrusted(address
, true);
294 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
295 return resolveRecipientTrusted(address
, false);
298 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
299 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
300 if (!pair
.second().isEmpty()) {
301 mergeRecipients(connection
, pair
.first(), pair
.second());
307 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
308 return resolveRecipientTrusted(new RecipientAddress(address
));
312 public RecipientId
resolveRecipientTrusted(
313 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
315 return resolveRecipientTrusted(new RecipientAddress(aci
, pni
, number
, Optional
.empty()));
319 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
320 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
324 public void storeContact(RecipientId recipientId
, final Contact contact
) {
325 try (final var connection
= database
.getConnection()) {
326 storeContact(connection
, recipientId
, contact
);
327 } catch (SQLException e
) {
328 throw new RuntimeException("Failed update recipient store", e
);
333 public Contact
getContact(RecipientId recipientId
) {
334 try (final var connection
= database
.getConnection()) {
335 return getContact(connection
, recipientId
);
336 } catch (SQLException e
) {
337 throw new RuntimeException("Failed read from recipient store", e
);
342 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
345 SELECT r._id, r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
347 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
349 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
350 try (final var connection
= database
.getConnection()) {
351 try (final var statement
= connection
.prepareStatement(sql
)) {
352 try (var result
= Utils
.executeQueryForStream(statement
,
353 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
354 getContactFromResultSet(resultSet
)))) {
355 return result
.toList();
358 } catch (SQLException e
) {
359 throw new RuntimeException("Failed read from recipient store", e
);
363 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
367 r.number, r.aci, r.pni, r.username,
368 r.profile_key, r.profile_key_credential,
369 r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
370 r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
375 ).formatted(TABLE_RECIPIENT
);
376 try (final var statement
= connection
.prepareStatement(sql
)) {
377 statement
.setLong(1, recipientId
.id());
378 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
382 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
386 r.number, r.aci, r.pni, r.username,
387 r.profile_key, r.profile_key_credential,
388 r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
389 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,
392 WHERE r.storage_id = ?
394 ).formatted(TABLE_RECIPIENT
);
395 try (final var statement
= connection
.prepareStatement(sql
)) {
396 statement
.setBytes(1, storageId
.getRaw());
397 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
401 public List
<Recipient
> getRecipients(
402 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
404 final var sqlWhere
= new ArrayList
<String
>();
406 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
407 sqlWhere
.add("r.hidden = FALSE");
409 if (blocked
.isPresent()) {
410 sqlWhere
.add("r.blocked = ?");
412 if (!recipientIds
.isEmpty()) {
413 final var recipientIdsCommaSeparated
= recipientIds
.stream()
414 .map(recipientId
-> String
.valueOf(recipientId
.id()))
415 .collect(Collectors
.joining(","));
416 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
421 r.number, r.aci, r.pni, r.username,
422 r.profile_key, r.profile_key_credential,
423 r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
424 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,
427 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
429 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
430 final var selfAddress
= selfAddressProvider
.getSelfAddress();
431 try (final var connection
= database
.getConnection()) {
432 try (final var statement
= connection
.prepareStatement(sql
)) {
433 if (blocked
.isPresent()) {
434 statement
.setBoolean(1, blocked
.get());
436 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
437 return result
.filter(r
-> name
.isEmpty() || (
438 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
439 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
440 if (r
.getAddress().matches(selfAddress
)) {
441 return Recipient
.newBuilder(r
)
442 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
449 } catch (SQLException e
) {
450 throw new RuntimeException("Failed read from recipient store", e
);
454 public Set
<String
> getAllNumbers() {
459 WHERE r.number IS NOT NULL
461 ).formatted(TABLE_RECIPIENT
);
462 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
463 try (final var connection
= database
.getConnection()) {
464 try (final var statement
= connection
.prepareStatement(sql
)) {
465 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
466 .filter(Objects
::nonNull
)
467 .filter(n
-> !n
.equals(selfNumber
))
472 } catch (NumberFormatException e
) {
476 .collect(Collectors
.toSet());
478 } catch (SQLException e
) {
479 throw new RuntimeException("Failed read from recipient store", e
);
483 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
486 SELECT r.aci, r.profile_key
488 WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL
490 ).formatted(TABLE_RECIPIENT
);
491 final var selfAci
= selfAddressProvider
.getSelfAddress().aci().orElse(null);
492 try (final var connection
= database
.getConnection()) {
493 try (final var statement
= connection
.prepareStatement(sql
)) {
494 return Utils
.executeQueryForStream(statement
, resultSet
-> {
495 final var aci
= ACI
.parseOrThrow(resultSet
.getString("aci"));
496 if (aci
.equals(selfAci
)) {
497 return new Pair
<>(aci
, selfProfileKeyProvider
.getSelfProfileKey());
499 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
500 return new Pair
<>(aci
, profileKey
);
501 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
503 } catch (SQLException e
) {
504 throw new RuntimeException("Failed read from recipient store", e
);
508 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
513 WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
515 ).formatted(TABLE_RECIPIENT
);
516 try (final var statement
= connection
.prepareStatement(sql
)) {
517 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
521 public void setMissingStorageIds() {
522 final var selectSql
= (
526 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
528 ).formatted(TABLE_RECIPIENT
);
529 final var updateSql
= (
535 ).formatted(TABLE_RECIPIENT
);
536 try (final var connection
= database
.getConnection()) {
537 connection
.setAutoCommit(false);
538 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
539 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
541 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
542 for (final var recipientId
: recipientIds
) {
543 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
544 updateStmt
.setLong(2, recipientId
.id());
545 updateStmt
.executeUpdate();
550 } catch (SQLException e
) {
551 throw new RuntimeException("Failed update recipient store", e
);
556 public void deleteContact(RecipientId recipientId
) {
557 storeContact(recipientId
, null);
560 public void deleteRecipientData(RecipientId recipientId
) {
561 logger
.debug("Deleting recipient data for {}", recipientId
);
562 synchronized (recipientsLock
) {
563 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
564 try (final var connection
= database
.getConnection()) {
565 connection
.setAutoCommit(false);
566 storeContact(connection
, recipientId
, null);
567 storeProfile(connection
, recipientId
, null);
568 storeProfileKey(connection
, recipientId
, null, false);
569 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
570 deleteRecipient(connection
, recipientId
);
572 } catch (SQLException e
) {
573 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
, RecipientId recipientId
, final ProfileKey profileKey
625 ) throws SQLException
{
626 storeProfileKey(connection
, recipientId
, profileKey
, true);
630 public void storeExpiringProfileKeyCredential(
631 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
633 try (final var connection
= database
.getConnection()) {
634 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
635 } catch (SQLException e
) {
636 throw new RuntimeException("Failed update recipient store", e
);
640 public void rotateSelfStorageId() {
641 try (final var connection
= database
.getConnection()) {
642 rotateSelfStorageId(connection
);
643 } catch (SQLException e
) {
644 throw new RuntimeException("Failed update recipient store", e
);
648 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
649 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
650 rotateStorageId(connection
, selfRecipientId
);
653 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
654 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
655 return rotateStorageId(connection
, selfRecipientId
);
658 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
661 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)
662 """.formatted(TABLE_RECIPIENT
);
663 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
664 try (final var statement
= connection
.prepareStatement(sql
)) {
665 statement
.setLong(1, selfRecipientId
.id());
666 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
670 public void updateStorageId(
671 Connection connection
, RecipientId recipientId
, StorageId storageId
672 ) throws SQLException
{
679 ).formatted(TABLE_RECIPIENT
);
680 try (final var statement
= connection
.prepareStatement(sql
)) {
681 statement
.setBytes(1, storageId
.getRaw());
682 statement
.setLong(2, recipientId
.id());
683 statement
.executeUpdate();
687 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
694 ).formatted(TABLE_RECIPIENT
);
695 try (final var statement
= connection
.prepareStatement(sql
)) {
696 for (final var entry
: storageIdMap
.entrySet()) {
697 statement
.setBytes(1, entry
.getValue().getRaw());
698 statement
.setLong(2, entry
.getKey().id());
699 statement
.executeUpdate();
704 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
705 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
706 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
709 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
712 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
713 """.formatted(TABLE_RECIPIENT
);
714 try (final var statement
= connection
.prepareStatement(sql
)) {
715 statement
.setLong(1, recipientId
.id());
716 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
717 if (storageId
.isPresent()) {
718 return storageId
.get();
721 return rotateStorageId(connection
, recipientId
);
724 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
725 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
726 updateStorageId(connection
, recipientId
, newStorageId
);
730 public void storeStorageRecord(
731 final Connection connection
,
732 final RecipientId recipientId
,
733 final StorageId storageId
,
734 final byte[] storageRecord
735 ) throws SQLException
{
736 final var deleteSql
= (
739 SET storage_id = NULL
742 ).formatted(TABLE_RECIPIENT
);
743 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
744 statement
.setBytes(1, storageId
.getRaw());
745 statement
.executeUpdate();
747 final var insertSql
= (
750 SET storage_id = ?, storage_record = ?
753 ).formatted(TABLE_RECIPIENT
);
754 try (final var statement
= connection
.prepareStatement(insertSql
)) {
755 statement
.setBytes(1, storageId
.getRaw());
756 if (storageRecord
== null) {
757 statement
.setNull(2, Types
.BLOB
);
759 statement
.setBytes(2, storageRecord
);
761 statement
.setLong(3, recipientId
.id());
762 statement
.executeUpdate();
766 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
767 logger
.debug("Migrating legacy recipients to database");
768 long start
= System
.nanoTime();
771 INSERT INTO %s (_id, number, aci)
774 ).formatted(TABLE_RECIPIENT
);
775 try (final var connection
= database
.getConnection()) {
776 connection
.setAutoCommit(false);
777 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
778 statement
.executeUpdate();
780 try (final var statement
= connection
.prepareStatement(sql
)) {
781 for (final var recipient
: recipients
.values()) {
782 statement
.setLong(1, recipient
.getRecipientId().id());
783 statement
.setString(2, recipient
.getAddress().number().orElse(null));
784 statement
.setString(3, recipient
.getAddress().aci().map(ACI
::toString
).orElse(null));
785 statement
.executeUpdate();
788 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
790 for (final var recipient
: recipients
.values()) {
791 if (recipient
.getContact() != null) {
792 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
794 if (recipient
.getProfile() != null) {
795 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
797 if (recipient
.getProfileKey() != null) {
798 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
800 if (recipient
.getExpiringProfileKeyCredential() != null) {
801 storeExpiringProfileKeyCredential(connection
,
802 recipient
.getRecipientId(),
803 recipient
.getExpiringProfileKeyCredential());
807 } catch (SQLException e
) {
808 throw new RuntimeException("Failed update recipient store", e
);
810 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
813 long getActualRecipientId(long recipientId
) {
814 while (recipientsMerged
.containsKey(recipientId
)) {
815 final var newRecipientId
= recipientsMerged
.get(recipientId
);
816 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
817 recipientId
= newRecipientId
;
822 public void storeContact(
823 final Connection connection
, final RecipientId recipientId
, final Contact contact
824 ) throws SQLException
{
828 SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?
831 ).formatted(TABLE_RECIPIENT
);
832 try (final var statement
= connection
.prepareStatement(sql
)) {
833 statement
.setString(1, contact
== null ?
null : contact
.givenName());
834 statement
.setString(2, contact
== null ?
null : contact
.familyName());
835 statement
.setString(3, contact
== null ?
null : contact
.nickName());
836 statement
.setInt(4, contact
== null ?
0 : contact
.messageExpirationTime());
837 statement
.setLong(5, contact
== null ?
0 : contact
.muteUntil());
838 statement
.setBoolean(6, contact
!= null && contact
.hideStory());
839 statement
.setBoolean(7, contact
!= null && contact
.isProfileSharingEnabled());
840 statement
.setString(8, contact
== null ?
null : contact
.color());
841 statement
.setBoolean(9, contact
!= null && contact
.isBlocked());
842 statement
.setBoolean(10, contact
!= null && contact
.isArchived());
843 if (contact
== null || contact
.unregisteredTimestamp() == null) {
844 statement
.setNull(11, Types
.INTEGER
);
846 statement
.setLong(11, contact
.unregisteredTimestamp());
848 statement
.setLong(12, recipientId
.id());
849 statement
.executeUpdate();
851 rotateStorageId(connection
, recipientId
);
854 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
855 final Connection connection
, final List
<StorageId
> storageIds
856 ) throws SQLException
{
860 SET storage_id = NULL
861 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
863 ).formatted(TABLE_RECIPIENT
);
865 try (final var statement
= connection
.prepareStatement(sql
)) {
866 for (final var storageId
: storageIds
) {
867 statement
.setBytes(1, storageId
.getRaw());
868 count
+= statement
.executeUpdate();
874 public void markUnregistered(final Set
<String
> unregisteredUsers
) {
875 logger
.debug("Marking {} numbers as unregistered", unregisteredUsers
.size());
876 try (final var connection
= database
.getConnection()) {
877 connection
.setAutoCommit(false);
878 for (final var number
: unregisteredUsers
) {
879 final var recipient
= findByNumber(connection
, number
);
880 if (recipient
.isPresent()) {
881 markUnregistered(connection
, recipient
.get().id());
885 } catch (SQLException e
) {
886 throw new RuntimeException("Failed update recipient store", e
);
890 private void markRegistered(
891 final Connection connection
, final RecipientId recipientId
892 ) throws SQLException
{
896 SET unregistered_timestamp = ?
899 ).formatted(TABLE_RECIPIENT
);
900 try (final var statement
= connection
.prepareStatement(sql
)) {
901 statement
.setNull(1, Types
.INTEGER
);
902 statement
.setLong(2, recipientId
.id());
903 statement
.executeUpdate();
907 private void markUnregistered(
908 final Connection connection
, final RecipientId recipientId
909 ) throws SQLException
{
913 SET unregistered_timestamp = ?
914 WHERE _id = ? AND unregistered_timestamp IS NULL
916 ).formatted(TABLE_RECIPIENT
);
917 try (final var statement
= connection
.prepareStatement(sql
)) {
918 statement
.setLong(1, System
.currentTimeMillis());
919 statement
.setLong(2, recipientId
.id());
920 statement
.executeUpdate();
924 private void storeExpiringProfileKeyCredential(
925 final Connection connection
,
926 final RecipientId recipientId
,
927 final ExpiringProfileKeyCredential profileKeyCredential
928 ) throws SQLException
{
932 SET profile_key_credential = ?
935 ).formatted(TABLE_RECIPIENT
);
936 try (final var statement
= connection
.prepareStatement(sql
)) {
937 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
938 statement
.setLong(2, recipientId
.id());
939 statement
.executeUpdate();
943 public void storeProfile(
944 final Connection connection
, final RecipientId recipientId
, final Profile profile
945 ) throws SQLException
{
949 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 = ?
952 ).formatted(TABLE_RECIPIENT
);
953 try (final var statement
= connection
.prepareStatement(sql
)) {
954 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
955 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
956 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
957 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
958 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
959 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
960 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
961 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
962 statement
.setString(9,
965 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
966 statement
.setLong(10, recipientId
.id());
967 statement
.executeUpdate();
969 rotateStorageId(connection
, recipientId
);
972 private void storeProfileKey(
973 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
974 ) throws SQLException
{
975 if (profileKey
!= null) {
976 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
977 if (profileKey
.equals(recipientProfileKey
)) {
978 final var recipientProfile
= getProfile(connection
, recipientId
);
979 if (recipientProfile
== null || (
980 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
981 && recipientProfile
.getUnidentifiedAccessMode()
982 != Profile
.UnidentifiedAccessMode
.DISABLED
992 SET profile_key = ?, profile_key_credential = NULL%s
995 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
996 try (final var statement
= connection
.prepareStatement(sql
)) {
997 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
998 statement
.setLong(2, recipientId
.id());
999 statement
.executeUpdate();
1001 rotateStorageId(connection
, recipientId
);
1004 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1005 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1006 synchronized (recipientsLock
) {
1007 try (final var connection
= database
.getConnection()) {
1008 connection
.setAutoCommit(false);
1009 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1010 connection
.commit();
1011 } catch (SQLException e
) {
1012 throw new RuntimeException("Failed update recipient store", e
);
1016 if (!pair
.second().isEmpty()) {
1017 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1018 try (final var connection
= database
.getConnection()) {
1019 connection
.setAutoCommit(false);
1020 mergeRecipients(connection
, pair
.first(), pair
.second());
1021 connection
.commit();
1022 } catch (SQLException e
) {
1023 throw new RuntimeException("Failed update recipient store", e
);
1026 return pair
.first();
1029 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1030 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1031 ) throws SQLException
{
1032 if (address
.hasSingleIdentifier() || (
1033 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1035 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1037 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1038 markRegistered(connection
, pair
.first());
1040 for (final var toBeMergedRecipientId
: pair
.second()) {
1041 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1047 private void mergeRecipients(
1048 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1049 ) throws SQLException
{
1050 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1051 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1052 deleteRecipient(connection
, toBeMergedRecipientId
);
1053 synchronized (recipientsLock
) {
1054 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1059 private RecipientId
resolveRecipientLocked(
1060 Connection connection
, RecipientAddress address
1061 ) throws SQLException
{
1062 final var byAci
= address
.aci().isEmpty()
1063 ? Optional
.<RecipientWithAddress
>empty()
1064 : findByServiceId(connection
, address
.aci().get());
1066 if (byAci
.isPresent()) {
1067 return byAci
.get().id();
1070 final var byPni
= address
.pni().isEmpty()
1071 ? Optional
.<RecipientWithAddress
>empty()
1072 : findByServiceId(connection
, address
.pni().get());
1074 if (byPni
.isPresent()) {
1075 return byPni
.get().id();
1078 final var byNumber
= address
.number().isEmpty()
1079 ? Optional
.<RecipientWithAddress
>empty()
1080 : findByNumber(connection
, address
.number().get());
1082 if (byNumber
.isPresent()) {
1083 return byNumber
.get().id();
1086 logger
.debug("Got new recipient, both serviceId and number are unknown");
1088 if (address
.serviceId().isEmpty()) {
1089 return addNewRecipient(connection
, address
);
1092 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1095 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1096 final var recipient
= findByServiceId(connection
, serviceId
);
1098 if (recipient
.isEmpty()) {
1099 logger
.debug("Got new recipient, serviceId is unknown");
1100 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1103 return recipient
.get().id();
1106 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1107 final var recipient
= findByNumber(connection
, number
);
1109 if (recipient
.isEmpty()) {
1110 logger
.debug("Got new recipient, number is unknown");
1111 return addNewRecipient(connection
, new RecipientAddress(number
));
1114 return recipient
.get().id();
1117 private RecipientId
addNewRecipient(
1118 final Connection connection
, final RecipientAddress address
1119 ) throws SQLException
{
1122 INSERT INTO %s (number, aci, pni, username)
1126 ).formatted(TABLE_RECIPIENT
);
1127 try (final var statement
= connection
.prepareStatement(sql
)) {
1128 statement
.setString(1, address
.number().orElse(null));
1129 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1130 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1131 statement
.setString(4, address
.username().orElse(null));
1132 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1133 if (generatedKey
.isPresent()) {
1134 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1135 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1138 throw new RuntimeException("Failed to add new recipient to database");
1143 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1144 synchronized (recipientsLock
) {
1145 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1149 SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL
1152 ).formatted(TABLE_RECIPIENT
);
1153 try (final var statement
= connection
.prepareStatement(sql
)) {
1154 statement
.setLong(1, recipientId
.id());
1155 statement
.executeUpdate();
1160 private void updateRecipientAddress(
1161 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1162 ) throws SQLException
{
1163 synchronized (recipientsLock
) {
1164 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1168 SET number = ?, aci = ?, pni = ?, username = ?
1171 ).formatted(TABLE_RECIPIENT
);
1172 try (final var statement
= connection
.prepareStatement(sql
)) {
1173 statement
.setString(1, address
.number().orElse(null));
1174 statement
.setString(2, address
.aci().map(ACI
::toString
).orElse(null));
1175 statement
.setString(3, address
.pni().map(PNI
::toString
).orElse(null));
1176 statement
.setString(4, address
.username().orElse(null));
1177 statement
.setLong(5, recipientId
.id());
1178 statement
.executeUpdate();
1180 rotateStorageId(connection
, recipientId
);
1184 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1190 ).formatted(TABLE_RECIPIENT
);
1191 try (final var statement
= connection
.prepareStatement(sql
)) {
1192 statement
.setLong(1, recipientId
.id());
1193 statement
.executeUpdate();
1197 private void mergeRecipientsLocked(
1198 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1199 ) throws SQLException
{
1200 final var contact
= getContact(connection
, recipientId
);
1201 if (contact
== null) {
1202 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1203 storeContact(connection
, recipientId
, toBeMergedContact
);
1206 final var profileKey
= getProfileKey(connection
, recipientId
);
1207 if (profileKey
== null) {
1208 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1209 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1212 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1213 if (profileKeyCredential
== null) {
1214 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1215 toBeMergedRecipientId
);
1216 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1219 final var profile
= getProfile(connection
, recipientId
);
1220 if (profile
== null) {
1221 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1222 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1225 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1228 private Optional
<RecipientWithAddress
> findByNumber(
1229 final Connection connection
, final String number
1230 ) throws SQLException
{
1232 SELECT r._id, r.number, r.aci, r.pni, r.username
1236 """.formatted(TABLE_RECIPIENT
);
1237 try (final var statement
= connection
.prepareStatement(sql
)) {
1238 statement
.setString(1, number
);
1239 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1243 private Optional
<RecipientWithAddress
> findByUsername(
1244 final Connection connection
, final String username
1245 ) throws SQLException
{
1247 SELECT r._id, r.number, r.aci, r.pni, r.username
1249 WHERE r.username = ?
1251 """.formatted(TABLE_RECIPIENT
);
1252 try (final var statement
= connection
.prepareStatement(sql
)) {
1253 statement
.setString(1, username
);
1254 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1258 private Optional
<RecipientWithAddress
> findByServiceId(
1259 final Connection connection
, final ServiceId serviceId
1260 ) throws SQLException
{
1261 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1262 if (recipientWithAddress
.isPresent()) {
1263 return recipientWithAddress
;
1266 SELECT r._id, r.number, r.aci, r.pni, r.username
1270 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.aci" : "r.pni");
1271 try (final var statement
= connection
.prepareStatement(sql
)) {
1272 statement
.setString(1, serviceId
.toString());
1273 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1274 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1275 return recipientWithAddress
;
1279 private Set
<RecipientWithAddress
> findAllByAddress(
1280 final Connection connection
, final RecipientAddress address
1281 ) throws SQLException
{
1283 SELECT r._id, r.number, r.aci, r.pni, r.username
1289 """.formatted(TABLE_RECIPIENT
);
1290 try (final var statement
= connection
.prepareStatement(sql
)) {
1291 statement
.setString(1, address
.aci().map(ServiceId
::toString
).orElse(null));
1292 statement
.setString(2, address
.pni().map(ServiceId
::toString
).orElse(null));
1293 statement
.setString(3, address
.number().orElse(null));
1294 statement
.setString(4, address
.username().orElse(null));
1295 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1296 .collect(Collectors
.toSet());
1300 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1303 SELECT r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
1305 WHERE r._id = ? AND (%s)
1307 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1308 try (final var statement
= connection
.prepareStatement(sql
)) {
1309 statement
.setLong(1, recipientId
.id());
1310 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1314 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1315 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1316 if (recipientId
.equals(selfRecipientId
)) {
1317 return selfProfileKeyProvider
.getSelfProfileKey();
1321 SELECT r.profile_key
1325 ).formatted(TABLE_RECIPIENT
);
1326 try (final var statement
= connection
.prepareStatement(sql
)) {
1327 statement
.setLong(1, recipientId
.id());
1328 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1332 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1333 final Connection connection
, final RecipientId recipientId
1334 ) throws SQLException
{
1337 SELECT r.profile_key_credential
1341 ).formatted(TABLE_RECIPIENT
);
1342 try (final var statement
= connection
.prepareStatement(sql
)) {
1343 statement
.setLong(1, recipientId
.id());
1344 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1349 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1352 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
1354 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1356 ).formatted(TABLE_RECIPIENT
);
1357 try (final var statement
= connection
.prepareStatement(sql
)) {
1358 statement
.setLong(1, recipientId
.id());
1359 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1363 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1364 final var aci
= Optional
.ofNullable(resultSet
.getString("aci")).map(ACI
::parseOrThrow
);
1365 final var pni
= Optional
.ofNullable(resultSet
.getString("pni")).map(PNI
::parseOrThrow
);
1366 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1367 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1368 return new RecipientAddress(aci
, pni
, number
, username
);
1371 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1372 return new RecipientId(resultSet
.getLong("_id"), this);
1375 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1376 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1377 getRecipientAddressFromResultSet(resultSet
));
1380 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1381 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1382 getRecipientAddressFromResultSet(resultSet
),
1383 getContactFromResultSet(resultSet
),
1384 getProfileKeyFromResultSet(resultSet
),
1385 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1386 getProfileFromResultSet(resultSet
),
1387 getStorageRecordFromResultSet(resultSet
));
1390 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1391 final var unregisteredTimestamp
= resultSet
.getLong("unregistered_timestamp");
1392 return new Contact(resultSet
.getString("given_name"),
1393 resultSet
.getString("family_name"),
1394 resultSet
.getString("nick_name"),
1395 resultSet
.getString("color"),
1396 resultSet
.getInt("expiration_time"),
1397 resultSet
.getLong("mute_until"),
1398 resultSet
.getBoolean("hide_story"),
1399 resultSet
.getBoolean("blocked"),
1400 resultSet
.getBoolean("archived"),
1401 resultSet
.getBoolean("profile_sharing"),
1402 resultSet
.getBoolean("hidden"),
1403 unregisteredTimestamp
== 0 ?
null : unregisteredTimestamp
);
1406 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1407 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1408 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1409 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1410 resultSet
.getString("profile_given_name"),
1411 resultSet
.getString("profile_family_name"),
1412 resultSet
.getString("profile_about"),
1413 resultSet
.getString("profile_about_emoji"),
1414 resultSet
.getString("profile_avatar_url_path"),
1415 resultSet
.getBytes("profile_mobile_coin_address"),
1416 profileUnidentifiedAccessMode
== null
1417 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1418 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1419 profileCapabilities
== null
1421 : Arrays
.stream(profileCapabilities
.split(","))
1422 .map(Profile
.Capability
::valueOfOrNull
)
1423 .filter(Objects
::nonNull
)
1424 .collect(Collectors
.toSet()));
1427 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1428 final var profileKey
= resultSet
.getBytes("profile_key");
1430 if (profileKey
== null) {
1434 return new ProfileKey(profileKey
);
1435 } catch (InvalidInputException ignored
) {
1440 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1441 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1443 if (profileKeyCredential
== null) {
1447 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1448 } catch (Throwable ignored
) {
1453 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1454 final var storageId
= resultSet
.getBytes("storage_id");
1455 return StorageId
.forContact(storageId
);
1458 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1459 return resultSet
.getBytes("storage_record");
1462 public interface RecipientMergeHandler
{
1464 void mergeRecipients(
1465 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1466 ) throws SQLException
;
1469 private class HelperStore
implements MergeRecipientHelper
.Store
{
1471 private final Connection connection
;
1473 public HelperStore(final Connection connection
) {
1474 this.connection
= connection
;
1478 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1479 return RecipientStore
.this.findAllByAddress(connection
, address
);
1483 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1484 return RecipientStore
.this.addNewRecipient(connection
, address
);
1488 public void updateRecipientAddress(
1489 final RecipientId recipientId
, final RecipientAddress address
1490 ) throws SQLException
{
1491 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1495 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1496 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);