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 import static org
.asamk
.signal
.manager
.config
.ServiceConfig
.UNREGISTERED_LIFESPAN
;
42 public class RecipientStore
implements RecipientIdCreator
, RecipientResolver
, RecipientTrustedResolver
, ContactsStore
, ProfileStore
{
44 private static final Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
45 private static final String TABLE_RECIPIENT
= "recipient";
46 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";
48 private final RecipientMergeHandler recipientMergeHandler
;
49 private final SelfAddressProvider selfAddressProvider
;
50 private final SelfProfileKeyProvider selfProfileKeyProvider
;
51 private final Database database
;
53 private final Object recipientsLock
= new Object();
54 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
56 private final Map
<ServiceId
, RecipientWithAddress
> recipientAddressCache
= new HashMap
<>();
58 public static void createSql(Connection connection
) throws SQLException
{
59 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
60 try (final var statement
= connection
.createStatement()) {
61 statement
.executeUpdate("""
62 CREATE TABLE recipient (
63 _id INTEGER PRIMARY KEY AUTOINCREMENT,
64 storage_id BLOB UNIQUE,
70 unregistered_timestamp INTEGER,
72 profile_key_credential BLOB,
78 expiration_time INTEGER NOT NULL DEFAULT 0,
79 blocked INTEGER NOT NULL DEFAULT FALSE,
80 archived INTEGER NOT NULL DEFAULT FALSE,
81 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
82 hidden INTEGER NOT NULL DEFAULT FALSE,
84 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
85 profile_given_name TEXT,
86 profile_family_name TEXT,
88 profile_about_emoji TEXT,
89 profile_avatar_url_path TEXT,
90 profile_mobile_coin_address BLOB,
91 profile_unidentified_access_mode TEXT,
92 profile_capabilities TEXT
98 public RecipientStore(
99 final RecipientMergeHandler recipientMergeHandler
,
100 final SelfAddressProvider selfAddressProvider
,
101 final SelfProfileKeyProvider selfProfileKeyProvider
,
102 final Database database
104 this.recipientMergeHandler
= recipientMergeHandler
;
105 this.selfAddressProvider
= selfAddressProvider
;
106 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
107 this.database
= database
;
110 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
113 SELECT r.number, r.uuid, r.pni, r.username
117 ).formatted(TABLE_RECIPIENT
);
118 try (final var connection
= database
.getConnection()) {
119 try (final var statement
= connection
.prepareStatement(sql
)) {
120 statement
.setLong(1, recipientId
.id());
121 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
123 } catch (SQLException e
) {
124 throw new RuntimeException("Failed read from recipient store", e
);
128 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
133 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
135 ).formatted(TABLE_RECIPIENT
);
136 try (final var connection
= database
.getConnection()) {
137 try (final var statement
= connection
.prepareStatement(sql
)) {
138 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
139 return result
.toList();
142 } catch (SQLException e
) {
143 throw new RuntimeException("Failed read from recipient store", e
);
148 public RecipientId
resolveRecipient(final long rawRecipientId
) {
155 ).formatted(TABLE_RECIPIENT
);
156 try (final var connection
= database
.getConnection()) {
157 try (final var statement
= connection
.prepareStatement(sql
)) {
158 statement
.setLong(1, rawRecipientId
);
159 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
161 } catch (SQLException e
) {
162 throw new RuntimeException("Failed read from recipient store", e
);
167 public RecipientId
resolveRecipient(final String identifier
) {
168 final var serviceId
= ServiceId
.parseOrNull(identifier
);
169 if (serviceId
!= null) {
170 return resolveRecipient(serviceId
);
172 return resolveRecipientByNumber(identifier
);
176 private RecipientId
resolveRecipientByNumber(final String number
) {
177 synchronized (recipientsLock
) {
178 final RecipientId recipientId
;
179 try (final var connection
= database
.getConnection()) {
180 connection
.setAutoCommit(false);
181 recipientId
= resolveRecipientLocked(connection
, number
);
183 } catch (SQLException e
) {
184 throw new RuntimeException("Failed read recipient store", e
);
191 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
192 synchronized (recipientsLock
) {
193 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
194 if (recipientWithAddress
!= null) {
195 return recipientWithAddress
.id();
197 try (final var connection
= database
.getConnection()) {
198 connection
.setAutoCommit(false);
199 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
202 } catch (SQLException e
) {
203 throw new RuntimeException("Failed read recipient store", e
);
209 * Should only be used for recipientIds from the database.
210 * Where the foreign key relations ensure a valid recipientId.
213 public RecipientId
create(final long recipientId
) {
214 return new RecipientId(recipientId
, this);
217 public RecipientId
resolveRecipientByNumber(
218 final String number
, Supplier
<ServiceId
> serviceIdSupplier
219 ) throws UnregisteredRecipientException
{
220 final Optional
<RecipientWithAddress
> byNumber
;
221 try (final var connection
= database
.getConnection()) {
222 byNumber
= findByNumber(connection
, number
);
223 } catch (SQLException e
) {
224 throw new RuntimeException("Failed read from recipient store", e
);
226 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
227 final var serviceId
= serviceIdSupplier
.get();
228 if (serviceId
== null) {
229 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
233 return resolveRecipient(serviceId
);
235 return byNumber
.get().id();
238 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
239 final Optional
<RecipientWithAddress
> byNumber
;
240 try (final var connection
= database
.getConnection()) {
241 byNumber
= findByNumber(connection
, number
);
242 } catch (SQLException e
) {
243 throw new RuntimeException("Failed read from recipient store", e
);
245 return byNumber
.map(RecipientWithAddress
::id
);
248 public RecipientId
resolveRecipientByUsername(
249 final String username
, Supplier
<ACI
> aciSupplier
250 ) throws UnregisteredRecipientException
{
251 final Optional
<RecipientWithAddress
> byUsername
;
252 try (final var connection
= database
.getConnection()) {
253 byUsername
= findByUsername(connection
, username
);
254 } catch (SQLException e
) {
255 throw new RuntimeException("Failed read from recipient store", e
);
257 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
258 final var aci
= aciSupplier
.get();
260 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
265 return resolveRecipientTrusted(aci
, username
);
267 return byUsername
.get().id();
270 public RecipientId
resolveRecipient(RecipientAddress address
) {
271 synchronized (recipientsLock
) {
272 final RecipientId recipientId
;
273 try (final var connection
= database
.getConnection()) {
274 connection
.setAutoCommit(false);
275 recipientId
= resolveRecipientLocked(connection
, address
);
277 } catch (SQLException e
) {
278 throw new RuntimeException("Failed read recipient store", e
);
284 public RecipientId
resolveRecipient(Connection connection
, RecipientAddress address
) throws SQLException
{
285 return resolveRecipientLocked(connection
, address
);
289 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
290 return resolveRecipientTrusted(address
, true);
294 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
295 return resolveRecipientTrusted(address
, false);
298 public RecipientId
resolveRecipientTrusted(Connection connection
, RecipientAddress address
) throws SQLException
{
299 final var pair
= resolveRecipientTrustedLocked(connection
, address
, false);
300 if (!pair
.second().isEmpty()) {
301 mergeRecipients(connection
, pair
.first(), pair
.second());
307 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
308 return resolveRecipientTrusted(new RecipientAddress(address
));
312 public RecipientId
resolveRecipientTrusted(
313 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
315 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
316 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()));
320 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
321 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
325 public void storeContact(RecipientId recipientId
, final Contact contact
) {
326 try (final var connection
= database
.getConnection()) {
327 storeContact(connection
, recipientId
, contact
);
328 } catch (SQLException e
) {
329 throw new RuntimeException("Failed update recipient store", e
);
334 public Contact
getContact(RecipientId recipientId
) {
335 try (final var connection
= database
.getConnection()) {
336 return getContact(connection
, recipientId
);
337 } catch (SQLException e
) {
338 throw new RuntimeException("Failed read from recipient store", e
);
343 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
346 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
348 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s AND r.hidden = FALSE
350 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
351 try (final var connection
= database
.getConnection()) {
352 try (final var statement
= connection
.prepareStatement(sql
)) {
353 try (var result
= Utils
.executeQueryForStream(statement
,
354 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
355 getContactFromResultSet(resultSet
)))) {
356 return result
.toList();
359 } catch (SQLException e
) {
360 throw new RuntimeException("Failed read from recipient store", e
);
364 public Recipient
getRecipient(Connection connection
, RecipientId recipientId
) throws SQLException
{
368 r.number, r.uuid, r.pni, r.username,
369 r.profile_key, r.profile_key_credential,
370 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
371 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,
376 ).formatted(TABLE_RECIPIENT
);
377 try (final var statement
= connection
.prepareStatement(sql
)) {
378 statement
.setLong(1, recipientId
.id());
379 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
383 public Recipient
getRecipient(Connection connection
, StorageId storageId
) throws SQLException
{
387 r.number, r.uuid, r.pni, r.username,
388 r.profile_key, r.profile_key_credential,
389 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
390 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,
393 WHERE r.storage_id = ?
395 ).formatted(TABLE_RECIPIENT
);
396 try (final var statement
= connection
.prepareStatement(sql
)) {
397 statement
.setBytes(1, storageId
.getRaw());
398 return Utils
.executeQuerySingleRow(statement
, this::getRecipientFromResultSet
);
402 public List
<Recipient
> getRecipients(
403 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
405 final var sqlWhere
= new ArrayList
<String
>();
407 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
408 sqlWhere
.add("r.hidden = FALSE");
410 if (blocked
.isPresent()) {
411 sqlWhere
.add("r.blocked = ?");
413 if (!recipientIds
.isEmpty()) {
414 final var recipientIdsCommaSeparated
= recipientIds
.stream()
415 .map(recipientId
-> String
.valueOf(recipientId
.id()))
416 .collect(Collectors
.joining(","));
417 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
422 r.number, r.uuid, r.pni, r.username,
423 r.profile_key, r.profile_key_credential,
424 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
425 r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
428 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
430 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
431 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId();
432 try (final var connection
= database
.getConnection()) {
433 try (final var statement
= connection
.prepareStatement(sql
)) {
434 if (blocked
.isPresent()) {
435 statement
.setBoolean(1, blocked
.get());
437 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
438 return result
.filter(r
-> name
.isEmpty() || (
439 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
440 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
441 if (r
.getAddress().serviceId().equals(selfServiceId
)) {
442 return Recipient
.newBuilder(r
)
443 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
450 } catch (SQLException e
) {
451 throw new RuntimeException("Failed read from recipient store", e
);
455 public Set
<String
> getAllNumbers() {
460 WHERE r.number IS NOT NULL
462 ).formatted(TABLE_RECIPIENT
);
463 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
464 try (final var connection
= database
.getConnection()) {
465 try (final var statement
= connection
.prepareStatement(sql
)) {
466 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
467 .filter(Objects
::nonNull
)
468 .filter(n
-> !n
.equals(selfNumber
))
473 } catch (NumberFormatException e
) {
477 .collect(Collectors
.toSet());
479 } catch (SQLException e
) {
480 throw new RuntimeException("Failed read from recipient store", e
);
484 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
487 SELECT r.uuid, r.profile_key
489 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
491 ).formatted(TABLE_RECIPIENT
);
492 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId().orElse(null);
493 try (final var connection
= database
.getConnection()) {
494 try (final var statement
= connection
.prepareStatement(sql
)) {
495 return Utils
.executeQueryForStream(statement
, resultSet
-> {
496 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
497 if (serviceId
.equals(selfServiceId
)) {
498 return new Pair
<>(serviceId
, selfProfileKeyProvider
.getSelfProfileKey());
500 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
501 return new Pair
<>(serviceId
, profileKey
);
502 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
504 } catch (SQLException e
) {
505 throw new RuntimeException("Failed read from recipient store", e
);
509 public List
<RecipientId
> getRecipientIds(Connection connection
) throws SQLException
{
514 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL)
516 ).formatted(TABLE_RECIPIENT
);
517 try (final var statement
= connection
.prepareStatement(sql
)) {
518 return Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
).toList();
522 public void setMissingStorageIds() {
523 final var selectSql
= (
527 WHERE r.storage_id IS NULL AND (r.unregistered_timestamp IS NULL OR r.unregistered_timestamp > ?)
529 ).formatted(TABLE_RECIPIENT
);
530 final var updateSql
= (
536 ).formatted(TABLE_RECIPIENT
);
537 try (final var connection
= database
.getConnection()) {
538 connection
.setAutoCommit(false);
539 try (final var selectStmt
= connection
.prepareStatement(selectSql
)) {
540 selectStmt
.setLong(1, System
.currentTimeMillis() - UNREGISTERED_LIFESPAN
);
541 final var recipientIds
= Utils
.executeQueryForStream(selectStmt
, this::getRecipientIdFromResultSet
)
543 try (final var updateStmt
= connection
.prepareStatement(updateSql
)) {
544 for (final var recipientId
: recipientIds
) {
545 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
546 updateStmt
.setLong(2, recipientId
.id());
547 updateStmt
.executeUpdate();
552 } catch (SQLException e
) {
553 throw new RuntimeException("Failed update recipient store", e
);
558 public void deleteContact(RecipientId recipientId
) {
559 storeContact(recipientId
, null);
562 public void deleteRecipientData(RecipientId recipientId
) {
563 logger
.debug("Deleting recipient data for {}", recipientId
);
564 synchronized (recipientsLock
) {
565 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
566 try (final var connection
= database
.getConnection()) {
567 connection
.setAutoCommit(false);
568 storeContact(connection
, recipientId
, null);
569 storeProfile(connection
, recipientId
, null);
570 storeProfileKey(connection
, recipientId
, null, false);
571 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
572 deleteRecipient(connection
, recipientId
);
574 } catch (SQLException e
) {
575 throw new RuntimeException("Failed update recipient store", e
);
581 public Profile
getProfile(final RecipientId recipientId
) {
582 try (final var connection
= database
.getConnection()) {
583 return getProfile(connection
, recipientId
);
584 } catch (SQLException e
) {
585 throw new RuntimeException("Failed read from recipient store", e
);
590 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
591 try (final var connection
= database
.getConnection()) {
592 return getProfileKey(connection
, recipientId
);
593 } catch (SQLException e
) {
594 throw new RuntimeException("Failed read from recipient store", e
);
599 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
600 try (final var connection
= database
.getConnection()) {
601 return getExpiringProfileKeyCredential(connection
, recipientId
);
602 } catch (SQLException e
) {
603 throw new RuntimeException("Failed read from recipient store", e
);
608 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
609 try (final var connection
= database
.getConnection()) {
610 storeProfile(connection
, recipientId
, profile
);
611 } catch (SQLException e
) {
612 throw new RuntimeException("Failed update recipient store", e
);
617 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
618 try (final var connection
= database
.getConnection()) {
619 storeProfileKey(connection
, recipientId
, profileKey
);
620 } catch (SQLException e
) {
621 throw new RuntimeException("Failed update recipient store", e
);
625 public void storeProfileKey(
626 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
627 ) throws SQLException
{
628 storeProfileKey(connection
, recipientId
, profileKey
, true);
632 public void storeExpiringProfileKeyCredential(
633 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
635 try (final var connection
= database
.getConnection()) {
636 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
637 } catch (SQLException e
) {
638 throw new RuntimeException("Failed update recipient store", e
);
642 public void rotateSelfStorageId() {
643 try (final var connection
= database
.getConnection()) {
644 rotateSelfStorageId(connection
);
645 } catch (SQLException e
) {
646 throw new RuntimeException("Failed update recipient store", e
);
650 public void rotateSelfStorageId(final Connection connection
) throws SQLException
{
651 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
652 rotateStorageId(connection
, selfRecipientId
);
655 public StorageId
rotateStorageId(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
656 final var selfRecipientId
= resolveRecipient(connection
, new RecipientAddress(serviceId
));
657 return rotateStorageId(connection
, selfRecipientId
);
660 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
663 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)
664 """.formatted(TABLE_RECIPIENT
);
665 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
666 try (final var statement
= connection
.prepareStatement(sql
)) {
667 statement
.setLong(1, selfRecipientId
.id());
668 return Utils
.executeQueryForStream(statement
, this::getContactStorageIdFromResultSet
).toList();
672 public void updateStorageId(
673 Connection connection
, RecipientId recipientId
, StorageId storageId
674 ) throws SQLException
{
681 ).formatted(TABLE_RECIPIENT
);
682 try (final var statement
= connection
.prepareStatement(sql
)) {
683 statement
.setBytes(1, storageId
.getRaw());
684 statement
.setLong(2, recipientId
.id());
685 statement
.executeUpdate();
689 public void updateStorageIds(Connection connection
, Map
<RecipientId
, StorageId
> storageIdMap
) throws SQLException
{
696 ).formatted(TABLE_RECIPIENT
);
697 try (final var statement
= connection
.prepareStatement(sql
)) {
698 for (final var entry
: storageIdMap
.entrySet()) {
699 statement
.setBytes(1, entry
.getValue().getRaw());
700 statement
.setLong(2, entry
.getKey().id());
701 statement
.executeUpdate();
706 public StorageId
getSelfStorageId(final Connection connection
) throws SQLException
{
707 final var selfRecipientId
= resolveRecipient(connection
, selfAddressProvider
.getSelfAddress());
708 return StorageId
.forAccount(getStorageId(connection
, selfRecipientId
).getRaw());
711 public StorageId
getStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
714 FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL
715 """.formatted(TABLE_RECIPIENT
);
716 try (final var statement
= connection
.prepareStatement(sql
)) {
717 statement
.setLong(1, recipientId
.id());
718 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getContactStorageIdFromResultSet
);
719 if (storageId
.isPresent()) {
720 return storageId
.get();
723 return rotateStorageId(connection
, recipientId
);
726 private StorageId
rotateStorageId(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
727 final var newStorageId
= StorageId
.forAccount(KeyUtils
.createRawStorageId());
728 updateStorageId(connection
, recipientId
, newStorageId
);
732 public void storeStorageRecord(
733 final Connection connection
,
734 final RecipientId recipientId
,
735 final StorageId storageId
,
736 final byte[] storageRecord
737 ) throws SQLException
{
741 SET storage_id = ?, storage_record = ?
744 ).formatted(TABLE_RECIPIENT
);
745 try (final var statement
= connection
.prepareStatement(sql
)) {
746 statement
.setBytes(1, storageId
.getRaw());
747 if (storageRecord
== null) {
748 statement
.setNull(2, Types
.BLOB
);
750 statement
.setBytes(2, storageRecord
);
752 statement
.setLong(3, recipientId
.id());
753 statement
.executeUpdate();
757 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
758 logger
.debug("Migrating legacy recipients to database");
759 long start
= System
.nanoTime();
762 INSERT INTO %s (_id, number, uuid)
765 ).formatted(TABLE_RECIPIENT
);
766 try (final var connection
= database
.getConnection()) {
767 connection
.setAutoCommit(false);
768 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
769 statement
.executeUpdate();
771 try (final var statement
= connection
.prepareStatement(sql
)) {
772 for (final var recipient
: recipients
.values()) {
773 statement
.setLong(1, recipient
.getRecipientId().id());
774 statement
.setString(2, recipient
.getAddress().number().orElse(null));
775 statement
.setBytes(3,
776 recipient
.getAddress()
778 .map(ServiceId
::getRawUuid
)
779 .map(UuidUtil
::toByteArray
)
781 statement
.executeUpdate();
784 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
786 for (final var recipient
: recipients
.values()) {
787 if (recipient
.getContact() != null) {
788 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
790 if (recipient
.getProfile() != null) {
791 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
793 if (recipient
.getProfileKey() != null) {
794 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
796 if (recipient
.getExpiringProfileKeyCredential() != null) {
797 storeExpiringProfileKeyCredential(connection
,
798 recipient
.getRecipientId(),
799 recipient
.getExpiringProfileKeyCredential());
803 } catch (SQLException e
) {
804 throw new RuntimeException("Failed update recipient store", e
);
806 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
809 long getActualRecipientId(long recipientId
) {
810 while (recipientsMerged
.containsKey(recipientId
)) {
811 final var newRecipientId
= recipientsMerged
.get(recipientId
);
812 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
813 recipientId
= newRecipientId
;
818 public void storeContact(
819 final Connection connection
, final RecipientId recipientId
, final Contact contact
820 ) throws SQLException
{
824 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
827 ).formatted(TABLE_RECIPIENT
);
828 try (final var statement
= connection
.prepareStatement(sql
)) {
829 statement
.setString(1, contact
== null ?
null : contact
.givenName());
830 statement
.setString(2, contact
== null ?
null : contact
.familyName());
831 statement
.setInt(3, contact
== null ?
0 : contact
.messageExpirationTime());
832 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
833 statement
.setString(5, contact
== null ?
null : contact
.color());
834 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
835 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
836 statement
.setLong(8, recipientId
.id());
837 statement
.executeUpdate();
839 rotateStorageId(connection
, recipientId
);
842 public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
843 final Connection connection
, final List
<StorageId
> storageIds
844 ) throws SQLException
{
848 SET storage_id = NULL
849 WHERE storage_id = ? AND storage_id IS NOT NULL AND unregistered_timestamp IS NOT NULL
851 ).formatted(TABLE_RECIPIENT
);
853 try (final var statement
= connection
.prepareStatement(sql
)) {
854 for (final var storageId
: storageIds
) {
855 statement
.setBytes(1, storageId
.getRaw());
856 count
+= statement
.executeUpdate();
862 public void markUnregistered(final Set
<String
> unregisteredUsers
) {
863 logger
.debug("Marking {} numbers as unregistered", unregisteredUsers
.size());
864 try (final var connection
= database
.getConnection()) {
865 connection
.setAutoCommit(false);
866 for (final var number
: unregisteredUsers
) {
867 final var recipient
= findByNumber(connection
, number
);
868 if (recipient
.isPresent()) {
869 markUnregistered(connection
, recipient
.get().id());
873 } catch (SQLException e
) {
874 throw new RuntimeException("Failed update recipient store", e
);
878 private void markRegistered(
879 final Connection connection
, final RecipientId recipientId
880 ) throws SQLException
{
884 SET unregistered_timestamp = ?
887 ).formatted(TABLE_RECIPIENT
);
888 try (final var statement
= connection
.prepareStatement(sql
)) {
889 statement
.setNull(1, Types
.INTEGER
);
890 statement
.setLong(2, recipientId
.id());
891 statement
.executeUpdate();
895 private void markUnregistered(
896 final Connection connection
, final RecipientId recipientId
897 ) throws SQLException
{
901 SET unregistered_timestamp = ?
902 WHERE _id = ? AND unregistered_timestamp IS NULL
904 ).formatted(TABLE_RECIPIENT
);
905 try (final var statement
= connection
.prepareStatement(sql
)) {
906 statement
.setLong(1, System
.currentTimeMillis());
907 statement
.setLong(2, recipientId
.id());
908 statement
.executeUpdate();
912 private void storeExpiringProfileKeyCredential(
913 final Connection connection
,
914 final RecipientId recipientId
,
915 final ExpiringProfileKeyCredential profileKeyCredential
916 ) throws SQLException
{
920 SET profile_key_credential = ?
923 ).formatted(TABLE_RECIPIENT
);
924 try (final var statement
= connection
.prepareStatement(sql
)) {
925 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
926 statement
.setLong(2, recipientId
.id());
927 statement
.executeUpdate();
931 public void storeProfile(
932 final Connection connection
, final RecipientId recipientId
, final Profile profile
933 ) throws SQLException
{
937 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 = ?
940 ).formatted(TABLE_RECIPIENT
);
941 try (final var statement
= connection
.prepareStatement(sql
)) {
942 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
943 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
944 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
945 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
946 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
947 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
948 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
949 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
950 statement
.setString(9,
953 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
954 statement
.setLong(10, recipientId
.id());
955 statement
.executeUpdate();
957 rotateStorageId(connection
, recipientId
);
960 private void storeProfileKey(
961 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
962 ) throws SQLException
{
963 if (profileKey
!= null) {
964 final var recipientProfileKey
= getProfileKey(connection
, recipientId
);
965 if (profileKey
.equals(recipientProfileKey
)) {
966 final var recipientProfile
= getProfile(connection
, recipientId
);
967 if (recipientProfile
== null || (
968 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
969 && recipientProfile
.getUnidentifiedAccessMode()
970 != Profile
.UnidentifiedAccessMode
.DISABLED
980 SET profile_key = ?, profile_key_credential = NULL%s
983 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
984 try (final var statement
= connection
.prepareStatement(sql
)) {
985 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
986 statement
.setLong(2, recipientId
.id());
987 statement
.executeUpdate();
989 rotateStorageId(connection
, recipientId
);
992 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
993 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
994 synchronized (recipientsLock
) {
995 try (final var connection
= database
.getConnection()) {
996 connection
.setAutoCommit(false);
997 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
999 } catch (SQLException e
) {
1000 throw new RuntimeException("Failed update recipient store", e
);
1004 if (!pair
.second().isEmpty()) {
1005 try (final var connection
= database
.getConnection()) {
1006 connection
.setAutoCommit(false);
1007 mergeRecipients(connection
, pair
.first(), pair
.second());
1008 connection
.commit();
1009 } catch (SQLException e
) {
1010 throw new RuntimeException("Failed update recipient store", e
);
1013 return pair
.first();
1016 private Pair
<RecipientId
, List
<RecipientId
>> resolveRecipientTrustedLocked(
1017 final Connection connection
, final RecipientAddress address
, final boolean isSelf
1018 ) throws SQLException
{
1019 if (address
.hasSingleIdentifier() || (
1020 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
1022 return new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
1024 final var pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
1025 markRegistered(connection
, pair
.first());
1027 for (final var toBeMergedRecipientId
: pair
.second()) {
1028 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
1034 private void mergeRecipients(
1035 final Connection connection
, final RecipientId recipientId
, final List
<RecipientId
> toBeMergedRecipientIds
1036 ) throws SQLException
{
1037 for (final var toBeMergedRecipientId
: toBeMergedRecipientIds
) {
1038 recipientMergeHandler
.mergeRecipients(connection
, recipientId
, toBeMergedRecipientId
);
1039 deleteRecipient(connection
, toBeMergedRecipientId
);
1040 synchronized (recipientsLock
) {
1041 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
1046 private RecipientId
resolveRecipientLocked(
1047 Connection connection
, RecipientAddress address
1048 ) throws SQLException
{
1049 final var aci
= address
.aci().isEmpty()
1050 ? Optional
.<RecipientWithAddress
>empty()
1051 : findByServiceId(connection
, address
.aci().get());
1053 if (aci
.isPresent()) {
1054 return aci
.get().id();
1057 final var byPni
= address
.pni().isEmpty()
1058 ? Optional
.<RecipientWithAddress
>empty()
1059 : findByServiceId(connection
, address
.pni().get());
1061 if (byPni
.isPresent()) {
1062 return byPni
.get().id();
1065 final var byNumber
= address
.number().isEmpty()
1066 ? Optional
.<RecipientWithAddress
>empty()
1067 : findByNumber(connection
, address
.number().get());
1069 if (byNumber
.isPresent()) {
1070 return byNumber
.get().id();
1073 logger
.debug("Got new recipient, both serviceId and number are unknown");
1075 if (address
.serviceId().isEmpty()) {
1076 return addNewRecipient(connection
, address
);
1079 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
1082 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
1083 final var recipient
= findByServiceId(connection
, serviceId
);
1085 if (recipient
.isEmpty()) {
1086 logger
.debug("Got new recipient, serviceId is unknown");
1087 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
1090 return recipient
.get().id();
1093 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
1094 final var recipient
= findByNumber(connection
, number
);
1096 if (recipient
.isEmpty()) {
1097 logger
.debug("Got new recipient, number is unknown");
1098 return addNewRecipient(connection
, new RecipientAddress(null, number
));
1101 return recipient
.get().id();
1104 private RecipientId
addNewRecipient(
1105 final Connection connection
, final RecipientAddress address
1106 ) throws SQLException
{
1109 INSERT INTO %s (number, uuid, pni, username)
1113 ).formatted(TABLE_RECIPIENT
);
1114 try (final var statement
= connection
.prepareStatement(sql
)) {
1115 statement
.setString(1, address
.number().orElse(null));
1116 statement
.setBytes(2, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1117 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1118 statement
.setString(4, address
.username().orElse(null));
1119 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
1120 if (generatedKey
.isPresent()) {
1121 final var recipientId
= new RecipientId(generatedKey
.get(), this);
1122 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
1125 throw new RuntimeException("Failed to add new recipient to database");
1130 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
1131 synchronized (recipientsLock
) {
1132 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1136 SET number = NULL, uuid = NULL, pni = NULL, username = NULL, storage_id = NULL
1139 ).formatted(TABLE_RECIPIENT
);
1140 try (final var statement
= connection
.prepareStatement(sql
)) {
1141 statement
.setLong(1, recipientId
.id());
1142 statement
.executeUpdate();
1147 private void updateRecipientAddress(
1148 Connection connection
, RecipientId recipientId
, final RecipientAddress address
1149 ) throws SQLException
{
1150 synchronized (recipientsLock
) {
1151 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
1155 SET number = ?, uuid = ?, pni = ?, username = ?
1158 ).formatted(TABLE_RECIPIENT
);
1159 try (final var statement
= connection
.prepareStatement(sql
)) {
1160 statement
.setString(1, address
.number().orElse(null));
1161 statement
.setBytes(2, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1162 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1163 statement
.setString(4, address
.username().orElse(null));
1164 statement
.setLong(5, recipientId
.id());
1165 statement
.executeUpdate();
1167 rotateStorageId(connection
, recipientId
);
1171 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1177 ).formatted(TABLE_RECIPIENT
);
1178 try (final var statement
= connection
.prepareStatement(sql
)) {
1179 statement
.setLong(1, recipientId
.id());
1180 statement
.executeUpdate();
1184 private void mergeRecipientsLocked(
1185 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1186 ) throws SQLException
{
1187 final var contact
= getContact(connection
, recipientId
);
1188 if (contact
== null) {
1189 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
1190 storeContact(connection
, recipientId
, toBeMergedContact
);
1193 final var profileKey
= getProfileKey(connection
, recipientId
);
1194 if (profileKey
== null) {
1195 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
1196 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
1199 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
1200 if (profileKeyCredential
== null) {
1201 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
1202 toBeMergedRecipientId
);
1203 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
1206 final var profile
= getProfile(connection
, recipientId
);
1207 if (profile
== null) {
1208 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
1209 storeProfile(connection
, recipientId
, toBeMergedProfile
);
1212 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
1215 private Optional
<RecipientWithAddress
> findByNumber(
1216 final Connection connection
, final String number
1217 ) throws SQLException
{
1219 SELECT r._id, r.number, r.uuid, r.pni, r.username
1223 """.formatted(TABLE_RECIPIENT
);
1224 try (final var statement
= connection
.prepareStatement(sql
)) {
1225 statement
.setString(1, number
);
1226 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1230 private Optional
<RecipientWithAddress
> findByUsername(
1231 final Connection connection
, final String username
1232 ) throws SQLException
{
1234 SELECT r._id, r.number, r.uuid, r.pni, r.username
1236 WHERE r.username = ?
1238 """.formatted(TABLE_RECIPIENT
);
1239 try (final var statement
= connection
.prepareStatement(sql
)) {
1240 statement
.setString(1, username
);
1241 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1245 private Optional
<RecipientWithAddress
> findByServiceId(
1246 final Connection connection
, final ServiceId serviceId
1247 ) throws SQLException
{
1248 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
1249 if (recipientWithAddress
.isPresent()) {
1250 return recipientWithAddress
;
1253 SELECT r._id, r.number, r.uuid, r.pni, r.username
1257 """.formatted(TABLE_RECIPIENT
, serviceId
instanceof ACI ?
"r.uuid" : "r.pni");
1258 try (final var statement
= connection
.prepareStatement(sql
)) {
1259 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.getRawUuid()));
1260 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
1261 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
1262 return recipientWithAddress
;
1266 private Set
<RecipientWithAddress
> findAllByAddress(
1267 final Connection connection
, final RecipientAddress address
1268 ) throws SQLException
{
1270 SELECT r._id, r.number, r.uuid, r.pni, r.username
1272 WHERE r.uuid = ?1 OR
1276 """.formatted(TABLE_RECIPIENT
);
1277 try (final var statement
= connection
.prepareStatement(sql
)) {
1278 statement
.setBytes(1, address
.aci().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1279 statement
.setBytes(2, address
.pni().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
1280 statement
.setString(3, address
.number().orElse(null));
1281 statement
.setString(4, address
.username().orElse(null));
1282 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
1283 .collect(Collectors
.toSet());
1287 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1290 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
1292 WHERE r._id = ? AND (%s)
1294 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
1295 try (final var statement
= connection
.prepareStatement(sql
)) {
1296 statement
.setLong(1, recipientId
.id());
1297 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
1301 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1302 final var selfRecipientId
= resolveRecipientLocked(connection
, selfAddressProvider
.getSelfAddress());
1303 if (recipientId
.equals(selfRecipientId
)) {
1304 return selfProfileKeyProvider
.getSelfProfileKey();
1308 SELECT r.profile_key
1312 ).formatted(TABLE_RECIPIENT
);
1313 try (final var statement
= connection
.prepareStatement(sql
)) {
1314 statement
.setLong(1, recipientId
.id());
1315 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1319 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1320 final Connection connection
, final RecipientId recipientId
1321 ) throws SQLException
{
1324 SELECT r.profile_key_credential
1328 ).formatted(TABLE_RECIPIENT
);
1329 try (final var statement
= connection
.prepareStatement(sql
)) {
1330 statement
.setLong(1, recipientId
.id());
1331 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1336 public Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1339 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
1341 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1343 ).formatted(TABLE_RECIPIENT
);
1344 try (final var statement
= connection
.prepareStatement(sql
)) {
1345 statement
.setLong(1, recipientId
.id());
1346 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1350 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1351 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(UuidUtil
::parseOrNull
).map(PNI
::from
);
1352 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
1353 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
1354 .equals(pni
.get().getRawUuid()) ? pni
.<ServiceId
>map(p
-> p
) : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
1355 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1356 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1357 return new RecipientAddress(serviceId
, pni
, number
, username
);
1360 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1361 return new RecipientId(resultSet
.getLong("_id"), this);
1364 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1365 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1366 getRecipientAddressFromResultSet(resultSet
));
1369 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1370 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1371 getRecipientAddressFromResultSet(resultSet
),
1372 getContactFromResultSet(resultSet
),
1373 getProfileKeyFromResultSet(resultSet
),
1374 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1375 getProfileFromResultSet(resultSet
),
1376 getStorageRecordFromResultSet(resultSet
));
1379 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1380 return new Contact(resultSet
.getString("given_name"),
1381 resultSet
.getString("family_name"),
1382 resultSet
.getString("color"),
1383 resultSet
.getInt("expiration_time"),
1384 resultSet
.getBoolean("blocked"),
1385 resultSet
.getBoolean("archived"),
1386 resultSet
.getBoolean("profile_sharing"),
1387 resultSet
.getBoolean("hidden"));
1390 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1391 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1392 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1393 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1394 resultSet
.getString("profile_given_name"),
1395 resultSet
.getString("profile_family_name"),
1396 resultSet
.getString("profile_about"),
1397 resultSet
.getString("profile_about_emoji"),
1398 resultSet
.getString("profile_avatar_url_path"),
1399 resultSet
.getBytes("profile_mobile_coin_address"),
1400 profileUnidentifiedAccessMode
== null
1401 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1402 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1403 profileCapabilities
== null
1405 : Arrays
.stream(profileCapabilities
.split(","))
1406 .map(Profile
.Capability
::valueOfOrNull
)
1407 .filter(Objects
::nonNull
)
1408 .collect(Collectors
.toSet()));
1411 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1412 final var profileKey
= resultSet
.getBytes("profile_key");
1414 if (profileKey
== null) {
1418 return new ProfileKey(profileKey
);
1419 } catch (InvalidInputException ignored
) {
1424 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1425 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1427 if (profileKeyCredential
== null) {
1431 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1432 } catch (Throwable ignored
) {
1437 private StorageId
getContactStorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1438 final var storageId
= resultSet
.getBytes("storage_id");
1439 return StorageId
.forContact(storageId
);
1442 private byte[] getStorageRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
1443 return resultSet
.getBytes("storage_record");
1446 public interface RecipientMergeHandler
{
1448 void mergeRecipients(
1449 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1450 ) throws SQLException
;
1453 private class HelperStore
implements MergeRecipientHelper
.Store
{
1455 private final Connection connection
;
1457 public HelperStore(final Connection connection
) {
1458 this.connection
= connection
;
1462 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1463 return RecipientStore
.this.findAllByAddress(connection
, address
);
1467 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1468 return RecipientStore
.this.addNewRecipient(connection
, address
);
1472 public void updateRecipientAddress(
1473 final RecipientId recipientId
, final RecipientAddress address
1474 ) throws SQLException
{
1475 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1479 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1480 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);