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,
69 profile_key_credential BLOB,
75 expiration_time INTEGER NOT NULL DEFAULT 0,
76 blocked INTEGER NOT NULL DEFAULT FALSE,
77 archived INTEGER NOT NULL DEFAULT FALSE,
78 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
79 hidden INTEGER NOT NULL DEFAULT FALSE,
81 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
82 profile_given_name TEXT,
83 profile_family_name TEXT,
85 profile_about_emoji TEXT,
86 profile_avatar_url_path TEXT,
87 profile_mobile_coin_address BLOB,
88 profile_unidentified_access_mode TEXT,
89 profile_capabilities TEXT
95 public RecipientStore(
96 final RecipientMergeHandler recipientMergeHandler
,
97 final SelfAddressProvider selfAddressProvider
,
98 final SelfProfileKeyProvider selfProfileKeyProvider
,
99 final Database database
101 this.recipientMergeHandler
= recipientMergeHandler
;
102 this.selfAddressProvider
= selfAddressProvider
;
103 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
104 this.database
= database
;
107 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
110 SELECT r.number, r.uuid, r.pni, r.username
114 ).formatted(TABLE_RECIPIENT
);
115 try (final var connection
= database
.getConnection()) {
116 try (final var statement
= connection
.prepareStatement(sql
)) {
117 statement
.setLong(1, recipientId
.id());
118 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
120 } catch (SQLException e
) {
121 throw new RuntimeException("Failed read from recipient store", e
);
125 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
130 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
132 ).formatted(TABLE_RECIPIENT
);
133 try (final var connection
= database
.getConnection()) {
134 try (final var statement
= connection
.prepareStatement(sql
)) {
135 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
136 return result
.toList();
139 } catch (SQLException e
) {
140 throw new RuntimeException("Failed read from recipient store", e
);
145 public RecipientId
resolveRecipient(final long rawRecipientId
) {
152 ).formatted(TABLE_RECIPIENT
);
153 try (final var connection
= database
.getConnection()) {
154 try (final var statement
= connection
.prepareStatement(sql
)) {
155 statement
.setLong(1, rawRecipientId
);
156 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
158 } catch (SQLException e
) {
159 throw new RuntimeException("Failed read from recipient store", e
);
164 public RecipientId
resolveRecipient(final String identifier
) {
165 final var serviceId
= ServiceId
.parseOrNull(identifier
);
166 if (serviceId
!= null) {
167 return resolveRecipient(serviceId
);
169 return resolveRecipientByNumber(identifier
);
173 private RecipientId
resolveRecipientByNumber(final String number
) {
174 synchronized (recipientsLock
) {
175 final RecipientId recipientId
;
176 try (final var connection
= database
.getConnection()) {
177 connection
.setAutoCommit(false);
178 recipientId
= resolveRecipientLocked(connection
, number
);
180 } catch (SQLException e
) {
181 throw new RuntimeException("Failed read recipient store", e
);
188 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
189 synchronized (recipientsLock
) {
190 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
191 if (recipientWithAddress
!= null) {
192 return recipientWithAddress
.id();
194 try (final var connection
= database
.getConnection()) {
195 connection
.setAutoCommit(false);
196 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
199 } catch (SQLException e
) {
200 throw new RuntimeException("Failed read recipient store", e
);
206 * Should only be used for recipientIds from the database.
207 * Where the foreign key relations ensure a valid recipientId.
210 public RecipientId
create(final long recipientId
) {
211 return new RecipientId(recipientId
, this);
214 public RecipientId
resolveRecipientByNumber(
215 final String number
, Supplier
<ServiceId
> serviceIdSupplier
216 ) throws UnregisteredRecipientException
{
217 final Optional
<RecipientWithAddress
> byNumber
;
218 try (final var connection
= database
.getConnection()) {
219 byNumber
= findByNumber(connection
, number
);
220 } catch (SQLException e
) {
221 throw new RuntimeException("Failed read from recipient store", e
);
223 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
224 final var serviceId
= serviceIdSupplier
.get();
225 if (serviceId
== null) {
226 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
230 return resolveRecipient(serviceId
);
232 return byNumber
.get().id();
235 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
236 final Optional
<RecipientWithAddress
> byNumber
;
237 try (final var connection
= database
.getConnection()) {
238 byNumber
= findByNumber(connection
, number
);
239 } catch (SQLException e
) {
240 throw new RuntimeException("Failed read from recipient store", e
);
242 return byNumber
.map(RecipientWithAddress
::id
);
245 public RecipientId
resolveRecipientByUsername(
246 final String username
, Supplier
<ACI
> aciSupplier
247 ) throws UnregisteredRecipientException
{
248 final Optional
<RecipientWithAddress
> byUsername
;
249 try (final var connection
= database
.getConnection()) {
250 byUsername
= findByUsername(connection
, username
);
251 } catch (SQLException e
) {
252 throw new RuntimeException("Failed read from recipient store", e
);
254 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
255 final var aci
= aciSupplier
.get();
257 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
262 return resolveRecipientTrusted(aci
, username
);
264 return byUsername
.get().id();
267 public RecipientId
resolveRecipient(RecipientAddress address
) {
268 synchronized (recipientsLock
) {
269 final RecipientId recipientId
;
270 try (final var connection
= database
.getConnection()) {
271 connection
.setAutoCommit(false);
272 recipientId
= resolveRecipientLocked(connection
, address
);
274 } catch (SQLException e
) {
275 throw new RuntimeException("Failed read recipient store", e
);
281 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
282 return resolveRecipientLocked(connection
, address
);
286 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
287 return resolveRecipientTrusted(address
, true);
291 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
292 return resolveRecipientTrusted(address
, false);
295 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
296 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
297 if (!pair
.second().isEmpty()) {
298 mergeRecipients(connection
, pair
.first(), pair
.second());
304 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
305 return resolveRecipientTrusted(new RecipientAddress(address
));
309 public RecipientId
resolveRecipientTrusted(
310 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
312 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
313 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()));
317 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
318 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
322 public void storeContact(RecipientId recipientId
, final Contact contact
) {
323 try (final var connection
= database
.getConnection()) {
324 storeContact(connection
, recipientId
, contact
);
325 } catch (SQLException e
) {
326 throw new RuntimeException("Failed update recipient store", e
);
331 public Contact
getContact(RecipientId recipientId
) {
332 try (final var connection
= database
.getConnection()) {
333 return getContact(connection
, recipientId
);
334 } catch (SQLException e
) {
335 throw new RuntimeException("Failed read from recipient store", e
);
340 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
343 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
345 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s AND r.hidden = FALSE
347 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
348 try (final var connection
= database
.getConnection()) {
349 try (final var statement
= connection
.prepareStatement(sql
)) {
350 try (var result
= Utils
.executeQueryForStream(statement
,
351 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
352 getContactFromResultSet(resultSet
)))) {
353 return result
.toList();
356 } catch (SQLException e
) {
357 throw new RuntimeException("Failed read from recipient store", e
);
361 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
365 r.number, r.uuid, r.pni, r.username,
366 r.profile_key, r.profile_key_credential,
367 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
368 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,
373 ).formatted(TABLE_RECIPIENT
);
374 try (final var statement
= connection
.prepareStatement(sql
)) {
375 statement
.setLong(1, recipientId
.id());
376 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
380 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
384 r.number, r.uuid, r.pni, r.username,
385 r.profile_key, r.profile_key_credential,
386 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
387 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,
390 WHERE r.storage_id = ?
392 ).formatted(TABLE_RECIPIENT
);
393 try (final var statement
= connection
.prepareStatement(sql
)) {
394 statement
.setBytes(1, storageId
.getRaw());
395 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
399 public List
<Recipient
> getRecipients(
400 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
402 final var sqlWhere
= new ArrayList
<String
>();
404 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
405 sqlWhere
.add("r.hidden = FALSE");
407 if (blocked
.isPresent()) {
408 sqlWhere
.add("r.blocked = ?");
410 if (!recipientIds
.isEmpty()) {
411 final var recipientIdsCommaSeparated
= recipientIds
.stream()
412 .map(recipientId
-> String
.valueOf(recipientId
.id()))
413 .collect(Collectors
.joining(","));
414 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
419 r.number, r.uuid, r.pni, r.username,
420 r.profile_key, r.profile_key_credential,
421 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
422 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,
425 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
427 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
428 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId();
429 try (final var connection
= database
.getConnection()) {
430 try (final var statement
= connection
.prepareStatement(sql
)) {
431 if (blocked
.isPresent()) {
432 statement
.setBoolean(1, blocked
.get());
434 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
435 return result
.filter(r
-> name
.isEmpty() || (
436 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
437 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
438 if (r
.getAddress().serviceId().equals(selfServiceId
)) {
439 return Recipient
.newBuilder(r
)
440 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
447 } catch (SQLException e
) {
448 throw new RuntimeException("Failed read from recipient store", e
);
452 public Set
<String
> getAllNumbers() {
457 WHERE r.number IS NOT NULL
459 ).formatted(TABLE_RECIPIENT
);
460 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
461 try (final var connection
= database
.getConnection()) {
462 try (final var statement
= connection
.prepareStatement(sql
)) {
463 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
464 .filter(Objects
::nonNull
)
465 .filter(n
-> !n
.equals(selfNumber
))
470 } catch (NumberFormatException e
) {
474 .collect(Collectors
.toSet());
476 } catch (SQLException e
) {
477 throw new RuntimeException("Failed read from recipient store", e
);
481 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
484 SELECT r.uuid, r.profile_key
486 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
488 ).formatted(TABLE_RECIPIENT
);
489 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId().orElse(null);
490 try (final var connection
= database
.getConnection()) {
491 try (final var statement
= connection
.prepareStatement(sql
)) {
492 return Utils
.executeQueryForStream(statement
, resultSet
-> {
493 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
494 if (serviceId
.equals(selfServiceId
)) {
495 return new Pair
<>(serviceId
, selfProfileKeyProvider
.getSelfProfileKey());
497 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
498 return new Pair
<>(serviceId
, profileKey
);
499 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
501 } catch (SQLException e
) {
502 throw new RuntimeException("Failed read from recipient store", e
);
506 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
511 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL)
513 ).formatted(TABLE_RECIPIENT
);
514 try (final var statement
= connection
.prepareStatement(sql
)) {
515 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
519 public void setMissingStorageIds() {
520 final var selectSql
= (
524 WHERE r.storage_id IS NULL
526 ).formatted(TABLE_RECIPIENT
);
527 final var updateSql
= (
533 ).formatted(TABLE_RECIPIENT
);
534 try (final var connection
= database
.getConnection()) {
535 connection
.setAutoCommit(false);
536 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
537 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
539 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
540 for (final var recipientId
: recipientIds
) {
541 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
542 updateStmt
.setLong(2, recipientId
.id());
543 updateStmt
.executeUpdate();
548 } catch (SQLException e
) {
549 throw new RuntimeException("Failed update recipient store", e
);
554 public void deleteContact(RecipientId recipientId
) {
555 storeContact(recipientId
, null);
558 public void deleteRecipientData(RecipientId recipientId
) {
559 logger
.debug("Deleting recipient data for {}", recipientId
);
560 synchronized (recipientsLock
) {
561 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
562 try (final var connection
= database
.getConnection()) {
563 connection
.setAutoCommit(false);
564 storeContact(connection
, recipientId
, null);
565 storeProfile(connection
, recipientId
, null);
566 storeProfileKey(connection
, recipientId
, null, false);
567 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
568 deleteRecipient(connection
, recipientId
);
570 } catch (SQLException e
) {
571 throw new RuntimeException("Failed update recipient store", e
);
577 public Profile
getProfile(final RecipientId recipientId
) {
578 try (final var connection
= database
.getConnection()) {
579 return getProfile(connection
, recipientId
);
580 } catch (SQLException e
) {
581 throw new RuntimeException("Failed read from recipient store", e
);
586 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
587 try (final var connection
= database
.getConnection()) {
588 return getProfileKey(connection
, recipientId
);
589 } catch (SQLException e
) {
590 throw new RuntimeException("Failed read from recipient store", e
);
595 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
596 try (final var connection
= database
.getConnection()) {
597 return getExpiringProfileKeyCredential(connection
, recipientId
);
598 } catch (SQLException e
) {
599 throw new RuntimeException("Failed read from recipient store", e
);
604 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
605 try (final var connection
= database
.getConnection()) {
606 storeProfile(connection
, recipientId
, profile
);
607 } catch (SQLException e
) {
608 throw new RuntimeException("Failed update recipient store", e
);
613 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
614 try (final var connection
= database
.getConnection()) {
615 storeProfileKey(connection
, recipientId
, profileKey
);
616 } catch (SQLException e
) {
617 throw new RuntimeException("Failed update recipient store", e
);
621 public void storeProfileKey(
622 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
623 ) throws SQLException
{
624 storeProfileKey(connection
, recipientId
, profileKey
, true);
628 public void storeExpiringProfileKeyCredential(
629 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
631 try (final var connection
= database
.getConnection()) {
632 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
633 } catch (SQLException e
) {
634 throw new RuntimeException("Failed update recipient store", e
);
638 public void rotateSelfStorageId() {
639 try (final var connection
= database
.getConnection()) {
640 rotateSelfStorageId(connection
);
641 } catch (SQLException e
) {
642 throw new RuntimeException("Failed update recipient store", e
);
646 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
647 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
648 rotateStorageId(connection
, selfRecipientId
);
651 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
652 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
653 return rotateStorageId(connection
, selfRecipientId
);
656 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
659 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)
660 """.formatted(TABLE_RECIPIENT
);
661 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
662 try (final var statement
= connection
.prepareStatement(sql
)) {
663 statement
.setLong(1, selfRecipientId
.id());
664 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
668 public void updateStorageId(
669 Connection connection
, RecipientId recipientId
, StorageId storageId
670 ) throws SQLException
{
677 ).formatted(TABLE_RECIPIENT
);
678 try (final var statement
= connection
.prepareStatement(sql
)) {
679 statement
.setBytes(1, storageId
.getRaw());
680 statement
.setLong(2, recipientId
.id());
681 statement
.executeUpdate();
685 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
692 ).formatted(TABLE_RECIPIENT
);
693 try (final var statement
= connection
.prepareStatement(sql
)) {
694 for (final var entry
: storageIdMap
.entrySet()) {
695 statement
.setBytes(1, entry
.getValue().getRaw());
696 statement
.setLong(2, entry
.getKey().id());
697 statement
.executeUpdate();
702 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
703 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
704 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
707 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
710 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
711 """.formatted(TABLE_RECIPIENT
);
712 try (final var statement
= connection
.prepareStatement(sql
)) {
713 statement
.setLong(1, recipientId
.id());
714 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
715 if (storageId
.isPresent()) {
716 return storageId
.get();
719 return rotateStorageId(connection
, recipientId
);
722 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
723 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
724 updateStorageId(connection
, recipientId
, newStorageId
);
728 public void storeStorageRecord(
729 final Connection connection
,
730 final RecipientId recipientId
,
731 final StorageId storageId
,
732 final byte[] storageRecord
733 ) throws SQLException
{
737 SET storage_id = ?, storage_record = ?
740 ).formatted(TABLE_RECIPIENT
);
741 try (final var statement
= connection
.prepareStatement(sql
)) {
742 statement
.setBytes(1, storageId
.getRaw());
743 if (storageRecord
== null) {
744 statement
.setNull(2, Types
.BLOB
);
746 statement
.setBytes(2, storageRecord
);
748 statement
.setLong(3, recipientId
.id());
749 statement
.executeUpdate();
753 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
754 logger
.debug("Migrating legacy recipients to database");
755 long start
= System
.nanoTime();
758 INSERT INTO %s (_id, number, uuid)
761 ).formatted(TABLE_RECIPIENT
);
762 try (final var connection
= database
.getConnection()) {
763 connection
.setAutoCommit(false);
764 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
765 statement
.executeUpdate();
767 try (final var statement
= connection
.prepareStatement(sql
)) {
768 for (final var recipient
: recipients
.values()) {
769 statement
.setLong(1, recipient
.getRecipientId().id());
770 statement
.setString(2, recipient
.getAddress().number().orElse(null));
771 statement
.setBytes(3,
772 recipient
.getAddress()
774 .map(ServiceId
::getRawUuid
)
775 .map(UuidUtil
::toByteArray
)
777 statement
.executeUpdate();
780 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
782 for (final var recipient
: recipients
.values()) {
783 if (recipient
.getContact() != null) {
784 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
786 if (recipient
.getProfile() != null) {
787 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
789 if (recipient
.getProfileKey() != null) {
790 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
792 if (recipient
.getExpiringProfileKeyCredential() != null) {
793 storeExpiringProfileKeyCredential(connection
,
794 recipient
.getRecipientId(),
795 recipient
.getExpiringProfileKeyCredential());
799 } catch (SQLException e
) {
800 throw new RuntimeException("Failed update recipient store", e
);
802 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
805 long getActualRecipientId(long recipientId
) {
806 while (recipientsMerged
.containsKey(recipientId
)) {
807 final var newRecipientId
= recipientsMerged
.get(recipientId
);
808 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
809 recipientId
= newRecipientId
;
814 public void storeContact(
815 final Connection connection
, final RecipientId recipientId
, final Contact contact
816 ) throws SQLException
{
820 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
823 ).formatted(TABLE_RECIPIENT
);
824 try (final var statement
= connection
.prepareStatement(sql
)) {
825 statement
.setString(1, contact
== null ?
null : contact
.givenName());
826 statement
.setString(2, contact
== null ?
null : contact
.familyName());
827 statement
.setInt(3, contact
== null ?
0 : contact
.messageExpirationTime());
828 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
829 statement
.setString(5, contact
== null ?
null : contact
.color());
830 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
831 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
832 statement
.setLong(8, recipientId
.id());
833 statement
.executeUpdate();
835 rotateStorageId(connection
, recipientId
);
838 private void storeExpiringProfileKeyCredential(
839 final Connection connection
,
840 final RecipientId recipientId
,
841 final ExpiringProfileKeyCredential profileKeyCredential
842 ) throws SQLException
{
846 SET profile_key_credential = ?
849 ).formatted(TABLE_RECIPIENT
);
850 try (final var statement
= connection
.prepareStatement(sql
)) {
851 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
852 statement
.setLong(2, recipientId
.id());
853 statement
.executeUpdate();
857 public void storeProfile(
858 final Connection connection
, final RecipientId recipientId
, final Profile profile
859 ) throws SQLException
{
863 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 = ?
866 ).formatted(TABLE_RECIPIENT
);
867 try (final var statement
= connection
.prepareStatement(sql
)) {
868 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
869 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
870 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
871 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
872 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
873 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
874 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
875 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
876 statement
.setString(9,
879 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
880 statement
.setLong(10, recipientId
.id());
881 statement
.executeUpdate();
883 rotateStorageId(connection
, recipientId
);
886 private void storeProfileKey(
887 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
888 ) throws SQLException
{
889 if (profileKey
!= null) {
890 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
891 if (profileKey
.equals(recipientProfileKey
)) {
892 final var recipientProfile
= getProfile(connection
, recipientId
);
893 if (recipientProfile
== null || (
894 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
895 && recipientProfile
.getUnidentifiedAccessMode()
896 != Profile
.UnidentifiedAccessMode
.DISABLED
906 SET profile_key = ?, profile_key_credential = NULL%s
909 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
910 try (final var statement
= connection
.prepareStatement(sql
)) {
911 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
912 statement
.setLong(2, recipientId
.id());
913 statement
.executeUpdate();
915 rotateStorageId(connection
, recipientId
);
918 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
919 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
920 synchronized (recipientsLock
) {
921 try (final var connection
= database
.getConnection()) {
922 connection
.setAutoCommit(false);
923 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
925 } catch (SQLException e
) {
926 throw new RuntimeException("Failed update recipient store", e
);
930 if (!pair
.second().isEmpty()) {
931 try (final var connection
= database
.getConnection()) {
932 connection
.setAutoCommit(false);
933 mergeRecipients(connection
, pair
.first(), pair
.second());
935 } catch (SQLException e
) {
936 throw new RuntimeException("Failed update recipient store", e
);
942 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
943 final Connection connection
, final RecipientAddress address
, final boolean isSelf
944 ) throws SQLException
{
945 if (address
.hasSingleIdentifier() || (
946 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
948 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
950 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
952 for (final var toBeMergedRecipientId
: pair
.second()) {
953 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
959 private void mergeRecipients(
960 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
961 ) throws SQLException
{
962 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
963 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
964 deleteRecipient(connection
, toBeMergedRecipientId
);
965 synchronized (recipientsLock
) {
966 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
971 private RecipientId
resolveRecipientLocked(
972 Connection connection
, RecipientAddress address
973 ) throws SQLException
{
974 final var aci
= address
.aci().isEmpty()
975 ? Optional
.<RecipientWithAddress
>empty()
976 : findByServiceId(connection
, address
.aci().get());
978 if (aci
.isPresent()) {
979 return aci
.get().id();
982 final var byPni
= address
.pni().isEmpty()
983 ? Optional
.<RecipientWithAddress
>empty()
984 : findByServiceId(connection
, address
.pni().get());
986 if (byPni
.isPresent()) {
987 return byPni
.get().id();
990 final var byNumber
= address
.number().isEmpty()
991 ? Optional
.<RecipientWithAddress
>empty()
992 : findByNumber(connection
, address
.number().get());
994 if (byNumber
.isPresent()) {
995 return byNumber
.get().id();
998 logger
.debug("Got new recipient, both serviceId and number are unknown");
1000 if (address
.serviceId().isEmpty()) {
1001 return addNewRecipient(connection
, address
);
1004 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1007 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1008 final var recipient
= findByServiceId(connection
, serviceId
);
1010 if (recipient
.isEmpty()) {
1011 logger
.debug("Got new recipient, serviceId is unknown");
1012 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1015 return recipient
.get().id();
1018 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1019 final var recipient
= findByNumber(connection
, number
);
1021 if (recipient
.isEmpty()) {
1022 logger
.debug("Got new recipient, number is unknown");
1023 return addNewRecipient(connection
, new RecipientAddress(null, number
));
1026 return recipient
.get().id();
1029 private RecipientId
addNewRecipient(
1030 final Connection connection
, final RecipientAddress address
1031 ) throws SQLException
{
1034 INSERT INTO %s (number, uuid, pni, username)
1038 ).formatted(TABLE_RECIPIENT
);
1039 try (final var statement
= connection
.prepareStatement(sql
)) {
1040 statement
.setString(1, address
.number().orElse(null));
1041 statement
.setBytes(2, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1042 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1043 statement
.setString(4, address
.username().orElse(null));
1044 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1045 if (generatedKey
.isPresent()) {
1046 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1047 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1050 throw new RuntimeException("Failed to add new recipient to database");
1055 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1056 synchronized (recipientsLock
) {
1057 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1061 SET number = NULL, uuid = NULL, pni = NULL, username = NULL, storage_id = NULL
1064 ).formatted(TABLE_RECIPIENT
);
1065 try (final var statement
= connection
.prepareStatement(sql
)) {
1066 statement
.setLong(1, recipientId
.id());
1067 statement
.executeUpdate();
1072 private void updateRecipientAddress(
1073 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1074 ) throws SQLException
{
1075 synchronized (recipientsLock
) {
1076 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1080 SET number = ?, uuid = ?, pni = ?, username = ?
1083 ).formatted(TABLE_RECIPIENT
);
1084 try (final var statement
= connection
.prepareStatement(sql
)) {
1085 statement
.setString(1, address
.number().orElse(null));
1086 statement
.setBytes(2, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1087 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1088 statement
.setString(4, address
.username().orElse(null));
1089 statement
.setLong(5, recipientId
.id());
1090 statement
.executeUpdate();
1092 rotateStorageId(connection
, recipientId
);
1096 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1102 ).formatted(TABLE_RECIPIENT
);
1103 try (final var statement
= connection
.prepareStatement(sql
)) {
1104 statement
.setLong(1, recipientId
.id());
1105 statement
.executeUpdate();
1109 private void mergeRecipientsLocked(
1110 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1111 ) throws SQLException
{
1112 final var contact
= getContact(connection
, recipientId
);
1113 if (contact
== null) {
1114 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1115 storeContact(connection
, recipientId
, toBeMergedContact
);
1118 final var profileKey
= getProfileKey(connection
, recipientId
);
1119 if (profileKey
== null) {
1120 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1121 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1124 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1125 if (profileKeyCredential
== null) {
1126 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1127 toBeMergedRecipientId
);
1128 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1131 final var profile
= getProfile(connection
, recipientId
);
1132 if (profile
== null) {
1133 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1134 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1137 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1140 private Optional
<RecipientWithAddress
> findByNumber(
1141 final Connection connection
, final String number
1142 ) throws SQLException
{
1144 SELECT r._id, r.number, r.uuid, r.pni, r.username
1148 """.formatted(TABLE_RECIPIENT
);
1149 try (final var statement
= connection
.prepareStatement(sql
)) {
1150 statement
.setString(1, number
);
1151 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1155 private Optional
<RecipientWithAddress
> findByUsername(
1156 final Connection connection
, final String username
1157 ) throws SQLException
{
1159 SELECT r._id, r.number, r.uuid, r.pni, r.username
1161 WHERE r.username = ?
1163 """.formatted(TABLE_RECIPIENT
);
1164 try (final var statement
= connection
.prepareStatement(sql
)) {
1165 statement
.setString(1, username
);
1166 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1170 private Optional
<RecipientWithAddress
> findByServiceId(
1171 final Connection connection
, final ServiceId serviceId
1172 ) throws SQLException
{
1173 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1174 if (recipientWithAddress
.isPresent()) {
1175 return recipientWithAddress
;
1178 SELECT r._id, r.number, r.uuid, r.pni, r.username
1182 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.uuid" : "r.pni");
1183 try (final var statement
= connection
.prepareStatement(sql
)) {
1184 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.getRawUuid()));
1185 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1186 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1187 return recipientWithAddress
;
1191 private Set
<RecipientWithAddress
> findAllByAddress(
1192 final Connection connection
, final RecipientAddress address
1193 ) throws SQLException
{
1195 SELECT r._id, r.number, r.uuid, r.pni, r.username
1197 WHERE r.uuid = ?1 OR
1201 """.formatted(TABLE_RECIPIENT
);
1202 try (final var statement
= connection
.prepareStatement(sql
)) {
1203 statement
.setBytes(1, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1204 statement
.setBytes(2, address
.pni().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1205 statement
.setString(3, address
.number().orElse(null));
1206 statement
.setString(4, address
.username().orElse(null));
1207 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1208 .collect(Collectors
.toSet());
1212 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1215 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
1217 WHERE r._id = ? AND (%s)
1219 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1220 try (final var statement
= connection
.prepareStatement(sql
)) {
1221 statement
.setLong(1, recipientId
.id());
1222 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1226 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1227 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1228 if (recipientId
.equals(selfRecipientId
)) {
1229 return selfProfileKeyProvider
.getSelfProfileKey();
1233 SELECT r.profile_key
1237 ).formatted(TABLE_RECIPIENT
);
1238 try (final var statement
= connection
.prepareStatement(sql
)) {
1239 statement
.setLong(1, recipientId
.id());
1240 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1244 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1245 final Connection connection
, final RecipientId recipientId
1246 ) throws SQLException
{
1249 SELECT r.profile_key_credential
1253 ).formatted(TABLE_RECIPIENT
);
1254 try (final var statement
= connection
.prepareStatement(sql
)) {
1255 statement
.setLong(1, recipientId
.id());
1256 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1261 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1264 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
1266 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1268 ).formatted(TABLE_RECIPIENT
);
1269 try (final var statement
= connection
.prepareStatement(sql
)) {
1270 statement
.setLong(1, recipientId
.id());
1271 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1275 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1276 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(UuidUtil
::parseOrNull
).map(PNI
::from
);
1277 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
1278 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
1279 .equals(pni
.get().getRawUuid()) ? pni
.<ServiceId
>map(p
-> p
) : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
1280 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1281 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1282 return new RecipientAddress(serviceId
, pni
, number
, username
);
1285 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1286 return new RecipientId(resultSet
.getLong("_id"), this);
1289 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1290 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1291 getRecipientAddressFromResultSet(resultSet
));
1294 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1295 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1296 getRecipientAddressFromResultSet(resultSet
),
1297 getContactFromResultSet(resultSet
),
1298 getProfileKeyFromResultSet(resultSet
),
1299 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1300 getProfileFromResultSet(resultSet
),
1301 getStorageRecordFromResultSet(resultSet
));
1304 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1305 return new Contact(resultSet
.getString("given_name"),
1306 resultSet
.getString("family_name"),
1307 resultSet
.getString("color"),
1308 resultSet
.getInt("expiration_time"),
1309 resultSet
.getBoolean("blocked"),
1310 resultSet
.getBoolean("archived"),
1311 resultSet
.getBoolean("profile_sharing"),
1312 resultSet
.getBoolean("hidden"));
1315 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1316 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1317 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1318 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1319 resultSet
.getString("profile_given_name"),
1320 resultSet
.getString("profile_family_name"),
1321 resultSet
.getString("profile_about"),
1322 resultSet
.getString("profile_about_emoji"),
1323 resultSet
.getString("profile_avatar_url_path"),
1324 resultSet
.getBytes("profile_mobile_coin_address"),
1325 profileUnidentifiedAccessMode
== null
1326 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1327 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1328 profileCapabilities
== null
1330 : Arrays
.stream(profileCapabilities
.split(","))
1331 .map(Profile
.Capability
::valueOfOrNull
)
1332 .filter(Objects
::nonNull
)
1333 .collect(Collectors
.toSet()));
1336 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1337 final var profileKey
= resultSet
.getBytes("profile_key");
1339 if (profileKey
== null) {
1343 return new ProfileKey(profileKey
);
1344 } catch (InvalidInputException ignored
) {
1349 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1350 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1352 if (profileKeyCredential
== null) {
1356 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1357 } catch (Throwable ignored
) {
1362 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1363 final var storageId
= resultSet
.getBytes("storage_id");
1364 return StorageId
.forContact(storageId
);
1367 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1368 return resultSet
.getBytes("storage_record");
1371 public interface RecipientMergeHandler
{
1373 void mergeRecipients(
1374 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1375 ) throws SQLException
;
1378 private class HelperStore
implements MergeRecipientHelper
.Store
{
1380 private final Connection connection
;
1382 public HelperStore(final Connection connection
) {
1383 this.connection
= connection
;
1387 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1388 return RecipientStore
.this.findAllByAddress(connection
, address
);
1392 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1393 return RecipientStore
.this.addNewRecipient(connection
, address
);
1397 public void updateRecipientAddress(
1398 final RecipientId recipientId
, final RecipientAddress address
1399 ) throws SQLException
{
1400 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1404 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1405 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);