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
;
22 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
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.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 Object recipientsLock
= new Object();
52 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
54 private final Map
<ServiceId
, RecipientWithAddress
> recipientAddressCache
= new HashMap
<>();
56 public static void createSql(Connection connection
) throws SQLException
{
57 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
58 try (final var statement
= connection
.createStatement()) {
59 statement
.executeUpdate("""
60 CREATE TABLE recipient (
61 _id INTEGER PRIMARY KEY AUTOINCREMENT,
62 storage_id BLOB UNIQUE,
68 unregistered_timestamp INTEGER,
70 profile_key_credential BLOB,
76 expiration_time INTEGER NOT NULL DEFAULT 0,
77 blocked INTEGER NOT NULL DEFAULT FALSE,
78 archived INTEGER NOT NULL DEFAULT FALSE,
79 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
80 hidden INTEGER NOT NULL DEFAULT FALSE,
82 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
83 profile_given_name TEXT,
84 profile_family_name TEXT,
86 profile_about_emoji TEXT,
87 profile_avatar_url_path TEXT,
88 profile_mobile_coin_address BLOB,
89 profile_unidentified_access_mode TEXT,
90 profile_capabilities TEXT
96 public RecipientStore(
97 final RecipientMergeHandler recipientMergeHandler
,
98 final SelfAddressProvider selfAddressProvider
,
99 final SelfProfileKeyProvider selfProfileKeyProvider
,
100 final Database database
102 this.recipientMergeHandler
= recipientMergeHandler
;
103 this.selfAddressProvider
= selfAddressProvider
;
104 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
105 this.database
= database
;
108 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
111 SELECT r.number, r.uuid, r.pni, r.username
115 ).formatted(TABLE_RECIPIENT
);
116 try (final var connection
= database
.getConnection()) {
117 try (final var statement
= connection
.prepareStatement(sql
)) {
118 statement
.setLong(1, recipientId
.id());
119 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
121 } catch (SQLException e
) {
122 throw new RuntimeException("Failed read from recipient store", e
);
126 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
131 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
133 ).formatted(TABLE_RECIPIENT
);
134 try (final var connection
= database
.getConnection()) {
135 try (final var statement
= connection
.prepareStatement(sql
)) {
136 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
137 return result
.toList();
140 } catch (SQLException e
) {
141 throw new RuntimeException("Failed read from recipient store", e
);
146 public RecipientId
resolveRecipient(final long rawRecipientId
) {
153 ).formatted(TABLE_RECIPIENT
);
154 try (final var connection
= database
.getConnection()) {
155 try (final var statement
= connection
.prepareStatement(sql
)) {
156 statement
.setLong(1, rawRecipientId
);
157 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
159 } catch (SQLException e
) {
160 throw new RuntimeException("Failed read from recipient store", e
);
165 public RecipientId
resolveRecipient(final String identifier
) {
166 final var serviceId
= ServiceId
.parseOrNull(identifier
);
167 if (serviceId
!= null) {
168 return resolveRecipient(serviceId
);
170 return resolveRecipientByNumber(identifier
);
174 private RecipientId
resolveRecipientByNumber(final String number
) {
175 synchronized (recipientsLock
) {
176 final RecipientId recipientId
;
177 try (final var connection
= database
.getConnection()) {
178 connection
.setAutoCommit(false);
179 recipientId
= resolveRecipientLocked(connection
, number
);
181 } catch (SQLException e
) {
182 throw new RuntimeException("Failed read recipient store", e
);
189 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
190 synchronized (recipientsLock
) {
191 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
192 if (recipientWithAddress
!= null) {
193 return recipientWithAddress
.id();
195 try (final var connection
= database
.getConnection()) {
196 connection
.setAutoCommit(false);
197 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
200 } catch (SQLException e
) {
201 throw new RuntimeException("Failed read recipient store", e
);
207 * Should only be used for recipientIds from the database.
208 * Where the foreign key relations ensure a valid recipientId.
211 public RecipientId
create(final long recipientId
) {
212 return new RecipientId(recipientId
, this);
215 public RecipientId
resolveRecipientByNumber(
216 final String number
, Supplier
<ServiceId
> serviceIdSupplier
217 ) throws UnregisteredRecipientException
{
218 final Optional
<RecipientWithAddress
> byNumber
;
219 try (final var connection
= database
.getConnection()) {
220 byNumber
= findByNumber(connection
, number
);
221 } catch (SQLException e
) {
222 throw new RuntimeException("Failed read from recipient store", e
);
224 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
225 final var serviceId
= serviceIdSupplier
.get();
226 if (serviceId
== null) {
227 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
231 return resolveRecipient(serviceId
);
233 return byNumber
.get().id();
236 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
237 final Optional
<RecipientWithAddress
> byNumber
;
238 try (final var connection
= database
.getConnection()) {
239 byNumber
= findByNumber(connection
, number
);
240 } catch (SQLException e
) {
241 throw new RuntimeException("Failed read from recipient store", e
);
243 return byNumber
.map(RecipientWithAddress
::id
);
246 public RecipientId
resolveRecipientByUsername(
247 final String username
, Supplier
<ACI
> aciSupplier
248 ) throws UnregisteredRecipientException
{
249 final Optional
<RecipientWithAddress
> byUsername
;
250 try (final var connection
= database
.getConnection()) {
251 byUsername
= findByUsername(connection
, username
);
252 } catch (SQLException e
) {
253 throw new RuntimeException("Failed read from recipient store", e
);
255 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
256 final var aci
= aciSupplier
.get();
258 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
263 return resolveRecipientTrusted(aci
, username
);
265 return byUsername
.get().id();
268 public RecipientId
resolveRecipient(RecipientAddress address
) {
269 synchronized (recipientsLock
) {
270 final RecipientId recipientId
;
271 try (final var connection
= database
.getConnection()) {
272 connection
.setAutoCommit(false);
273 recipientId
= resolveRecipientLocked(connection
, address
);
275 } catch (SQLException e
) {
276 throw new RuntimeException("Failed read recipient store", e
);
282 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
283 return resolveRecipientLocked(connection
, address
);
287 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
288 return resolveRecipientTrusted(address
, true);
292 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
293 return resolveRecipientTrusted(address
, false);
296 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
297 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
298 if (!pair
.second().isEmpty()) {
299 mergeRecipients(connection
, pair
.first(), pair
.second());
305 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
306 return resolveRecipientTrusted(new RecipientAddress(address
));
310 public RecipientId
resolveRecipientTrusted(
311 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
313 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
314 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()));
318 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
319 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
323 public void storeContact(RecipientId recipientId
, final Contact contact
) {
324 try (final var connection
= database
.getConnection()) {
325 storeContact(connection
, recipientId
, contact
);
326 } catch (SQLException e
) {
327 throw new RuntimeException("Failed update recipient store", e
);
332 public Contact
getContact(RecipientId recipientId
) {
333 try (final var connection
= database
.getConnection()) {
334 return getContact(connection
, recipientId
);
335 } catch (SQLException e
) {
336 throw new RuntimeException("Failed read from recipient store", e
);
341 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
344 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
346 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s AND r.hidden = FALSE
348 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
349 try (final var connection
= database
.getConnection()) {
350 try (final var statement
= connection
.prepareStatement(sql
)) {
351 try (var result
= Utils
.executeQueryForStream(statement
,
352 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
353 getContactFromResultSet(resultSet
)))) {
354 return result
.toList();
357 } catch (SQLException e
) {
358 throw new RuntimeException("Failed read from recipient store", e
);
362 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
366 r.number, r.uuid, r.pni, r.username,
367 r.profile_key, r.profile_key_credential,
368 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
369 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,
374 ).formatted(TABLE_RECIPIENT
);
375 try (final var statement
= connection
.prepareStatement(sql
)) {
376 statement
.setLong(1, recipientId
.id());
377 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
381 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
385 r.number, r.uuid, r.pni, r.username,
386 r.profile_key, r.profile_key_credential,
387 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
388 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,
391 WHERE r.storage_id = ?
393 ).formatted(TABLE_RECIPIENT
);
394 try (final var statement
= connection
.prepareStatement(sql
)) {
395 statement
.setBytes(1, storageId
.getRaw());
396 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
400 public List
<Recipient
> getRecipients(
401 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
403 final var sqlWhere
= new ArrayList
<String
>();
405 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
406 sqlWhere
.add("r.hidden = FALSE");
408 if (blocked
.isPresent()) {
409 sqlWhere
.add("r.blocked = ?");
411 if (!recipientIds
.isEmpty()) {
412 final var recipientIdsCommaSeparated
= recipientIds
.stream()
413 .map(recipientId
-> String
.valueOf(recipientId
.id()))
414 .collect(Collectors
.joining(","));
415 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
420 r.number, r.uuid, r.pni, r.username,
421 r.profile_key, r.profile_key_credential,
422 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
423 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,
426 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
428 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
429 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId();
430 try (final var connection
= database
.getConnection()) {
431 try (final var statement
= connection
.prepareStatement(sql
)) {
432 if (blocked
.isPresent()) {
433 statement
.setBoolean(1, blocked
.get());
435 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
436 return result
.filter(r
-> name
.isEmpty() || (
437 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
438 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
439 if (r
.getAddress().serviceId().equals(selfServiceId
)) {
440 return Recipient
.newBuilder(r
)
441 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
448 } catch (SQLException e
) {
449 throw new RuntimeException("Failed read from recipient store", e
);
453 public Set
<String
> getAllNumbers() {
458 WHERE r.number IS NOT NULL
460 ).formatted(TABLE_RECIPIENT
);
461 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
462 try (final var connection
= database
.getConnection()) {
463 try (final var statement
= connection
.prepareStatement(sql
)) {
464 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
465 .filter(Objects
::nonNull
)
466 .filter(n
-> !n
.equals(selfNumber
))
471 } catch (NumberFormatException e
) {
475 .collect(Collectors
.toSet());
477 } catch (SQLException e
) {
478 throw new RuntimeException("Failed read from recipient store", e
);
482 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
485 SELECT r.uuid, r.profile_key
487 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
489 ).formatted(TABLE_RECIPIENT
);
490 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId().orElse(null);
491 try (final var connection
= database
.getConnection()) {
492 try (final var statement
= connection
.prepareStatement(sql
)) {
493 return Utils
.executeQueryForStream(statement
, resultSet
-> {
494 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
495 if (serviceId
.equals(selfServiceId
)) {
496 return new Pair
<>(serviceId
, selfProfileKeyProvider
.getSelfProfileKey());
498 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
499 return new Pair
<>(serviceId
, profileKey
);
500 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
502 } catch (SQLException e
) {
503 throw new RuntimeException("Failed read from recipient store", e
);
507 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
512 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL)
514 ).formatted(TABLE_RECIPIENT
);
515 try (final var statement
= connection
.prepareStatement(sql
)) {
516 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
520 public void setMissingStorageIds() {
521 final var selectSql
= (
525 WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
527 ).formatted(TABLE_RECIPIENT
);
528 final var updateSql
= (
534 ).formatted(TABLE_RECIPIENT
);
535 try (final var connection
= database
.getConnection()) {
536 connection
.setAutoCommit(false);
537 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
538 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
540 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
541 for (final var recipientId
: recipientIds
) {
542 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
543 updateStmt
.setLong(2, recipientId
.id());
544 updateStmt
.executeUpdate();
549 } catch (SQLException e
) {
550 throw new RuntimeException("Failed update recipient store", e
);
555 public void deleteContact(RecipientId recipientId
) {
556 storeContact(recipientId
, null);
559 public void deleteRecipientData(RecipientId recipientId
) {
560 logger
.debug("Deleting recipient data for {}", recipientId
);
561 synchronized (recipientsLock
) {
562 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
563 try (final var connection
= database
.getConnection()) {
564 connection
.setAutoCommit(false);
565 storeContact(connection
, recipientId
, null);
566 storeProfile(connection
, recipientId
, null);
567 storeProfileKey(connection
, recipientId
, null, false);
568 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
569 deleteRecipient(connection
, recipientId
);
571 } catch (SQLException e
) {
572 throw new RuntimeException("Failed update recipient store", e
);
578 public Profile
getProfile(final RecipientId recipientId
) {
579 try (final var connection
= database
.getConnection()) {
580 return getProfile(connection
, recipientId
);
581 } catch (SQLException e
) {
582 throw new RuntimeException("Failed read from recipient store", e
);
587 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
588 try (final var connection
= database
.getConnection()) {
589 return getProfileKey(connection
, recipientId
);
590 } catch (SQLException e
) {
591 throw new RuntimeException("Failed read from recipient store", e
);
596 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
597 try (final var connection
= database
.getConnection()) {
598 return getExpiringProfileKeyCredential(connection
, recipientId
);
599 } catch (SQLException e
) {
600 throw new RuntimeException("Failed read from recipient store", e
);
605 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
606 try (final var connection
= database
.getConnection()) {
607 storeProfile(connection
, recipientId
, profile
);
608 } catch (SQLException e
) {
609 throw new RuntimeException("Failed update recipient store", e
);
614 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
615 try (final var connection
= database
.getConnection()) {
616 storeProfileKey(connection
, recipientId
, profileKey
);
617 } catch (SQLException e
) {
618 throw new RuntimeException("Failed update recipient store", e
);
622 public void storeProfileKey(
623 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
624 ) throws SQLException
{
625 storeProfileKey(connection
, recipientId
, profileKey
, true);
629 public void storeExpiringProfileKeyCredential(
630 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
632 try (final var connection
= database
.getConnection()) {
633 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
634 } catch (SQLException e
) {
635 throw new RuntimeException("Failed update recipient store", e
);
639 public void rotateSelfStorageId() {
640 try (final var connection
= database
.getConnection()) {
641 rotateSelfStorageId(connection
);
642 } catch (SQLException e
) {
643 throw new RuntimeException("Failed update recipient store", e
);
647 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
648 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
649 rotateStorageId(connection
, selfRecipientId
);
652 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
653 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
654 return rotateStorageId(connection
, selfRecipientId
);
657 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
660 FROM %s r WHERE r.storage_id IS NOT NULL AND r._id != ? AND (r.uuid IS NOT NULL OR r.pni IS NOT NULL)
661 """.formatted(TABLE_RECIPIENT
);
662 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
663 try (final var statement
= connection
.prepareStatement(sql
)) {
664 statement
.setLong(1, selfRecipientId
.id());
665 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
669 public void updateStorageId(
670 Connection connection
, RecipientId recipientId
, StorageId storageId
671 ) throws SQLException
{
678 ).formatted(TABLE_RECIPIENT
);
679 try (final var statement
= connection
.prepareStatement(sql
)) {
680 statement
.setBytes(1, storageId
.getRaw());
681 statement
.setLong(2, recipientId
.id());
682 statement
.executeUpdate();
686 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
693 ).formatted(TABLE_RECIPIENT
);
694 try (final var statement
= connection
.prepareStatement(sql
)) {
695 for (final var entry
: storageIdMap
.entrySet()) {
696 statement
.setBytes(1, entry
.getValue().getRaw());
697 statement
.setLong(2, entry
.getKey().id());
698 statement
.executeUpdate();
703 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
704 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
705 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
708 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
711 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
712 """.formatted(TABLE_RECIPIENT
);
713 try (final var statement
= connection
.prepareStatement(sql
)) {
714 statement
.setLong(1, recipientId
.id());
715 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
716 if (storageId
.isPresent()) {
717 return storageId
.get();
720 return rotateStorageId(connection
, recipientId
);
723 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
724 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
725 updateStorageId(connection
, recipientId
, newStorageId
);
729 public void storeStorageRecord(
730 final Connection connection
,
731 final RecipientId recipientId
,
732 final StorageId storageId
,
733 final byte[] storageRecord
734 ) throws SQLException
{
735 final var deleteSql
= (
738 SET storage_id = NULL
741 ).formatted(TABLE_RECIPIENT
);
742 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
743 statement
.setBytes(1, storageId
.getRaw());
744 statement
.executeUpdate();
746 final var insertSql
= (
749 SET storage_id = ?, storage_record = ?
752 ).formatted(TABLE_RECIPIENT
);
753 try (final var statement
= connection
.prepareStatement(insertSql
)) {
754 statement
.setBytes(1, storageId
.getRaw());
755 if (storageRecord
== null) {
756 statement
.setNull(2, Types
.BLOB
);
758 statement
.setBytes(2, storageRecord
);
760 statement
.setLong(3, recipientId
.id());
761 statement
.executeUpdate();
765 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
766 logger
.debug("Migrating legacy recipients to database");
767 long start
= System
.nanoTime();
770 INSERT INTO %s (_id, number, uuid)
773 ).formatted(TABLE_RECIPIENT
);
774 try (final var connection
= database
.getConnection()) {
775 connection
.setAutoCommit(false);
776 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
777 statement
.executeUpdate();
779 try (final var statement
= connection
.prepareStatement(sql
)) {
780 for (final var recipient
: recipients
.values()) {
781 statement
.setLong(1, recipient
.getRecipientId().id());
782 statement
.setString(2, recipient
.getAddress().number().orElse(null));
783 statement
.setBytes(3,
784 recipient
.getAddress()
786 .map(ServiceId
::getRawUuid
)
787 .map(UuidUtil
::toByteArray
)
789 statement
.executeUpdate();
792 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
794 for (final var recipient
: recipients
.values()) {
795 if (recipient
.getContact() != null) {
796 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
798 if (recipient
.getProfile() != null) {
799 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
801 if (recipient
.getProfileKey() != null) {
802 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
804 if (recipient
.getExpiringProfileKeyCredential() != null) {
805 storeExpiringProfileKeyCredential(connection
,
806 recipient
.getRecipientId(),
807 recipient
.getExpiringProfileKeyCredential());
811 } catch (SQLException e
) {
812 throw new RuntimeException("Failed update recipient store", e
);
814 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
817 long getActualRecipientId(long recipientId
) {
818 while (recipientsMerged
.containsKey(recipientId
)) {
819 final var newRecipientId
= recipientsMerged
.get(recipientId
);
820 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
821 recipientId
= newRecipientId
;
826 public void storeContact(
827 final Connection connection
, final RecipientId recipientId
, final Contact contact
828 ) throws SQLException
{
832 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
835 ).formatted(TABLE_RECIPIENT
);
836 try (final var statement
= connection
.prepareStatement(sql
)) {
837 statement
.setString(1, contact
== null ?
null : contact
.givenName());
838 statement
.setString(2, contact
== null ?
null : contact
.familyName());
839 statement
.setInt(3, contact
== null ?
0 : contact
.messageExpirationTime());
840 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
841 statement
.setString(5, contact
== null ?
null : contact
.color());
842 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
843 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
844 statement
.setLong(8, recipientId
.id());
845 statement
.executeUpdate();
847 rotateStorageId(connection
, recipientId
);
850 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
851 final Connection connection
, final List
<StorageId
> storageIds
852 ) throws SQLException
{
856 SET storage_id = NULL
857 WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL
859 ).formatted(TABLE_RECIPIENT
);
861 try (final var statement
= connection
.prepareStatement(sql
)) {
862 for (final var storageId
: storageIds
) {
863 statement
.setBytes(1, storageId
.getRaw());
864 count
+= statement
.executeUpdate();
870 public void markUnregistered(final Set
<String
> unregisteredUsers
) {
871 logger
.debug("Marking {} numbers as unregistered", unregisteredUsers
.size());
872 try (final var connection
= database
.getConnection()) {
873 connection
.setAutoCommit(false);
874 for (final var number
: unregisteredUsers
) {
875 final var recipient
= findByNumber(connection
, number
);
876 if (recipient
.isPresent()) {
877 markUnregistered(connection
, recipient
.get().id());
881 } catch (SQLException e
) {
882 throw new RuntimeException("Failed update recipient store", e
);
886 private void markRegistered(
887 final Connection connection
, final RecipientId recipientId
888 ) throws SQLException
{
892 SET unregistered_timestamp = ?
895 ).formatted(TABLE_RECIPIENT
);
896 try (final var statement
= connection
.prepareStatement(sql
)) {
897 statement
.setNull(1, Types
.INTEGER
);
898 statement
.setLong(2, recipientId
.id());
899 statement
.executeUpdate();
903 private void markUnregistered(
904 final Connection connection
, final RecipientId recipientId
905 ) throws SQLException
{
909 SET unregistered_timestamp = ?
910 WHERE _id = ? AND unregistered_timestamp IS NULL
912 ).formatted(TABLE_RECIPIENT
);
913 try (final var statement
= connection
.prepareStatement(sql
)) {
914 statement
.setLong(1, System
.currentTimeMillis());
915 statement
.setLong(2, recipientId
.id());
916 statement
.executeUpdate();
920 private void storeExpiringProfileKeyCredential(
921 final Connection connection
,
922 final RecipientId recipientId
,
923 final ExpiringProfileKeyCredential profileKeyCredential
924 ) throws SQLException
{
928 SET profile_key_credential = ?
931 ).formatted(TABLE_RECIPIENT
);
932 try (final var statement
= connection
.prepareStatement(sql
)) {
933 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
934 statement
.setLong(2, recipientId
.id());
935 statement
.executeUpdate();
939 public void storeProfile(
940 final Connection connection
, final RecipientId recipientId
, final Profile profile
941 ) throws SQLException
{
945 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 = ?
948 ).formatted(TABLE_RECIPIENT
);
949 try (final var statement
= connection
.prepareStatement(sql
)) {
950 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
951 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
952 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
953 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
954 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
955 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
956 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
957 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
958 statement
.setString(9,
961 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
962 statement
.setLong(10, recipientId
.id());
963 statement
.executeUpdate();
965 rotateStorageId(connection
, recipientId
);
968 private void storeProfileKey(
969 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
970 ) throws SQLException
{
971 if (profileKey
!= null) {
972 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
973 if (profileKey
.equals(recipientProfileKey
)) {
974 final var recipientProfile
= getProfile(connection
, recipientId
);
975 if (recipientProfile
== null || (
976 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
977 && recipientProfile
.getUnidentifiedAccessMode()
978 != Profile
.UnidentifiedAccessMode
.DISABLED
988 SET profile_key = ?, profile_key_credential = NULL%s
991 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
992 try (final var statement
= connection
.prepareStatement(sql
)) {
993 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
994 statement
.setLong(2, recipientId
.id());
995 statement
.executeUpdate();
997 rotateStorageId(connection
, recipientId
);
1000 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
1001 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
1002 synchronized (recipientsLock
) {
1003 try (final var connection
= database
.getConnection()) {
1004 connection
.setAutoCommit(false);
1005 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
1006 connection
.commit();
1007 } catch (SQLException e
) {
1008 throw new RuntimeException("Failed update recipient store", e
);
1012 if (!pair
.second().isEmpty()) {
1013 logger
.debug("Resolved address {}, merging {} other recipients", address
, pair
.second().size());
1014 try (final var connection
= database
.getConnection()) {
1015 connection
.setAutoCommit(false);
1016 mergeRecipients(connection
, pair
.first(), pair
.second());
1017 connection
.commit();
1018 } catch (SQLException e
) {
1019 throw new RuntimeException("Failed update recipient store", e
);
1022 return pair
.first();
1025 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1026 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1027 ) throws SQLException
{
1028 if (address
.hasSingleIdentifier() || (
1029 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1031 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1033 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1034 markRegistered(connection
, pair
.first());
1036 for (final var toBeMergedRecipientId
: pair
.second()) {
1037 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1043 private void mergeRecipients(
1044 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1045 ) throws SQLException
{
1046 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1047 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1048 deleteRecipient(connection
, toBeMergedRecipientId
);
1049 synchronized (recipientsLock
) {
1050 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1055 private RecipientId
resolveRecipientLocked(
1056 Connection connection
, RecipientAddress address
1057 ) throws SQLException
{
1058 final var aci
= address
.aci().isEmpty()
1059 ? Optional
.<RecipientWithAddress
>empty()
1060 : findByServiceId(connection
, address
.aci().get());
1062 if (aci
.isPresent()) {
1063 return aci
.get().id();
1066 final var byPni
= address
.pni().isEmpty()
1067 ? Optional
.<RecipientWithAddress
>empty()
1068 : findByServiceId(connection
, address
.pni().get());
1070 if (byPni
.isPresent()) {
1071 return byPni
.get().id();
1074 final var byNumber
= address
.number().isEmpty()
1075 ? Optional
.<RecipientWithAddress
>empty()
1076 : findByNumber(connection
, address
.number().get());
1078 if (byNumber
.isPresent()) {
1079 return byNumber
.get().id();
1082 logger
.debug("Got new recipient, both serviceId and number are unknown");
1084 if (address
.serviceId().isEmpty()) {
1085 return addNewRecipient(connection
, address
);
1088 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1091 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1092 final var recipient
= findByServiceId(connection
, serviceId
);
1094 if (recipient
.isEmpty()) {
1095 logger
.debug("Got new recipient, serviceId is unknown");
1096 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1099 return recipient
.get().id();
1102 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1103 final var recipient
= findByNumber(connection
, number
);
1105 if (recipient
.isEmpty()) {
1106 logger
.debug("Got new recipient, number is unknown");
1107 return addNewRecipient(connection
, new RecipientAddress(null, number
));
1110 return recipient
.get().id();
1113 private RecipientId
addNewRecipient(
1114 final Connection connection
, final RecipientAddress address
1115 ) throws SQLException
{
1118 INSERT INTO %s (number, uuid, pni, username)
1122 ).formatted(TABLE_RECIPIENT
);
1123 try (final var statement
= connection
.prepareStatement(sql
)) {
1124 statement
.setString(1, address
.number().orElse(null));
1125 statement
.setBytes(2, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1126 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1127 statement
.setString(4, address
.username().orElse(null));
1128 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1129 if (generatedKey
.isPresent()) {
1130 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1131 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1134 throw new RuntimeException("Failed to add new recipient to database");
1139 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1140 synchronized (recipientsLock
) {
1141 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1145 SET number = NULL, uuid = NULL, pni = NULL, username = NULL, storage_id = NULL
1148 ).formatted(TABLE_RECIPIENT
);
1149 try (final var statement
= connection
.prepareStatement(sql
)) {
1150 statement
.setLong(1, recipientId
.id());
1151 statement
.executeUpdate();
1156 private void updateRecipientAddress(
1157 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1158 ) throws SQLException
{
1159 synchronized (recipientsLock
) {
1160 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1164 SET number = ?, uuid = ?, pni = ?, username = ?
1167 ).formatted(TABLE_RECIPIENT
);
1168 try (final var statement
= connection
.prepareStatement(sql
)) {
1169 statement
.setString(1, address
.number().orElse(null));
1170 statement
.setBytes(2, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1171 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1172 statement
.setString(4, address
.username().orElse(null));
1173 statement
.setLong(5, recipientId
.id());
1174 statement
.executeUpdate();
1176 rotateStorageId(connection
, recipientId
);
1180 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1186 ).formatted(TABLE_RECIPIENT
);
1187 try (final var statement
= connection
.prepareStatement(sql
)) {
1188 statement
.setLong(1, recipientId
.id());
1189 statement
.executeUpdate();
1193 private void mergeRecipientsLocked(
1194 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1195 ) throws SQLException
{
1196 final var contact
= getContact(connection
, recipientId
);
1197 if (contact
== null) {
1198 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1199 storeContact(connection
, recipientId
, toBeMergedContact
);
1202 final var profileKey
= getProfileKey(connection
, recipientId
);
1203 if (profileKey
== null) {
1204 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1205 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1208 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1209 if (profileKeyCredential
== null) {
1210 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1211 toBeMergedRecipientId
);
1212 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1215 final var profile
= getProfile(connection
, recipientId
);
1216 if (profile
== null) {
1217 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1218 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1221 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1224 private Optional
<RecipientWithAddress
> findByNumber(
1225 final Connection connection
, final String number
1226 ) throws SQLException
{
1228 SELECT r._id, r.number, r.uuid, r.pni, r.username
1232 """.formatted(TABLE_RECIPIENT
);
1233 try (final var statement
= connection
.prepareStatement(sql
)) {
1234 statement
.setString(1, number
);
1235 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1239 private Optional
<RecipientWithAddress
> findByUsername(
1240 final Connection connection
, final String username
1241 ) throws SQLException
{
1243 SELECT r._id, r.number, r.uuid, r.pni, r.username
1245 WHERE r.username = ?
1247 """.formatted(TABLE_RECIPIENT
);
1248 try (final var statement
= connection
.prepareStatement(sql
)) {
1249 statement
.setString(1, username
);
1250 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1254 private Optional
<RecipientWithAddress
> findByServiceId(
1255 final Connection connection
, final ServiceId serviceId
1256 ) throws SQLException
{
1257 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1258 if (recipientWithAddress
.isPresent()) {
1259 return recipientWithAddress
;
1262 SELECT r._id, r.number, r.uuid, r.pni, r.username
1266 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.uuid" : "r.pni");
1267 try (final var statement
= connection
.prepareStatement(sql
)) {
1268 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.getRawUuid()));
1269 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1270 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1271 return recipientWithAddress
;
1275 private Set
<RecipientWithAddress
> findAllByAddress(
1276 final Connection connection
, final RecipientAddress address
1277 ) throws SQLException
{
1279 SELECT r._id, r.number, r.uuid, r.pni, r.username
1281 WHERE r.uuid = ?1 OR
1285 """.formatted(TABLE_RECIPIENT
);
1286 try (final var statement
= connection
.prepareStatement(sql
)) {
1287 statement
.setBytes(1, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1288 statement
.setBytes(2, address
.pni().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1289 statement
.setString(3, address
.number().orElse(null));
1290 statement
.setString(4, address
.username().orElse(null));
1291 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1292 .collect(Collectors
.toSet());
1296 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1299 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
1301 WHERE r._id = ? AND (%s)
1303 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1304 try (final var statement
= connection
.prepareStatement(sql
)) {
1305 statement
.setLong(1, recipientId
.id());
1306 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1310 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1311 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1312 if (recipientId
.equals(selfRecipientId
)) {
1313 return selfProfileKeyProvider
.getSelfProfileKey();
1317 SELECT r.profile_key
1321 ).formatted(TABLE_RECIPIENT
);
1322 try (final var statement
= connection
.prepareStatement(sql
)) {
1323 statement
.setLong(1, recipientId
.id());
1324 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1328 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1329 final Connection connection
, final RecipientId recipientId
1330 ) throws SQLException
{
1333 SELECT r.profile_key_credential
1337 ).formatted(TABLE_RECIPIENT
);
1338 try (final var statement
= connection
.prepareStatement(sql
)) {
1339 statement
.setLong(1, recipientId
.id());
1340 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1345 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1348 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
1350 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1352 ).formatted(TABLE_RECIPIENT
);
1353 try (final var statement
= connection
.prepareStatement(sql
)) {
1354 statement
.setLong(1, recipientId
.id());
1355 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1359 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1360 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(UuidUtil
::parseOrNull
).map(PNI
::from
);
1361 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
1362 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
1363 .equals(pni
.get().getRawUuid()) ? pni
.<ServiceId
>map(p
-> p
) : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
1364 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1365 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1366 return new RecipientAddress(serviceId
, pni
, number
, username
);
1369 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1370 return new RecipientId(resultSet
.getLong("_id"), this);
1373 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1374 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1375 getRecipientAddressFromResultSet(resultSet
));
1378 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1379 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1380 getRecipientAddressFromResultSet(resultSet
),
1381 getContactFromResultSet(resultSet
),
1382 getProfileKeyFromResultSet(resultSet
),
1383 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1384 getProfileFromResultSet(resultSet
),
1385 getStorageRecordFromResultSet(resultSet
));
1388 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1389 return new Contact(resultSet
.getString("given_name"),
1390 resultSet
.getString("family_name"),
1391 resultSet
.getString("color"),
1392 resultSet
.getInt("expiration_time"),
1393 resultSet
.getBoolean("blocked"),
1394 resultSet
.getBoolean("archived"),
1395 resultSet
.getBoolean("profile_sharing"),
1396 resultSet
.getBoolean("hidden"));
1399 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1400 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1401 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1402 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1403 resultSet
.getString("profile_given_name"),
1404 resultSet
.getString("profile_family_name"),
1405 resultSet
.getString("profile_about"),
1406 resultSet
.getString("profile_about_emoji"),
1407 resultSet
.getString("profile_avatar_url_path"),
1408 resultSet
.getBytes("profile_mobile_coin_address"),
1409 profileUnidentifiedAccessMode
== null
1410 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1411 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1412 profileCapabilities
== null
1414 : Arrays
.stream(profileCapabilities
.split(","))
1415 .map(Profile
.Capability
::valueOfOrNull
)
1416 .filter(Objects
::nonNull
)
1417 .collect(Collectors
.toSet()));
1420 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1421 final var profileKey
= resultSet
.getBytes("profile_key");
1423 if (profileKey
== null) {
1427 return new ProfileKey(profileKey
);
1428 } catch (InvalidInputException ignored
) {
1433 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1434 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1436 if (profileKeyCredential
== null) {
1440 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1441 } catch (Throwable ignored
) {
1446 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1447 final var storageId
= resultSet
.getBytes("storage_id");
1448 return StorageId
.forContact(storageId
);
1451 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1452 return resultSet
.getBytes("storage_record");
1455 public interface RecipientMergeHandler
{
1457 void mergeRecipients(
1458 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1459 ) throws SQLException
;
1462 private class HelperStore
implements MergeRecipientHelper
.Store
{
1464 private final Connection connection
;
1466 public HelperStore(final Connection connection
) {
1467 this.connection
= connection
;
1471 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1472 return RecipientStore
.this.findAllByAddress(connection
, address
);
1476 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1477 return RecipientStore
.this.addNewRecipient(connection
, address
);
1481 public void updateRecipientAddress(
1482 final RecipientId recipientId
, final RecipientAddress address
1483 ) throws SQLException
{
1484 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1488 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1489 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);