1 package org
.asamk
.signal
.manager
.storage
.recipients
;
3 import org
.asamk
.signal
.manager
.api
.Pair
;
4 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
5 import org
.asamk
.signal
.manager
.storage
.Database
;
6 import org
.asamk
.signal
.manager
.storage
.Utils
;
7 import org
.asamk
.signal
.manager
.storage
.contacts
.ContactsStore
;
8 import org
.asamk
.signal
.manager
.storage
.profiles
.ProfileStore
;
9 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
10 import org
.signal
.libsignal
.zkgroup
.profiles
.ExpiringProfileKeyCredential
;
11 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
12 import org
.slf4j
.Logger
;
13 import org
.slf4j
.LoggerFactory
;
14 import org
.whispersystems
.signalservice
.api
.push
.ACI
;
15 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
16 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
18 import java
.sql
.Connection
;
19 import java
.sql
.ResultSet
;
20 import java
.sql
.SQLException
;
21 import java
.util
.ArrayList
;
22 import java
.util
.Arrays
;
23 import java
.util
.Collection
;
24 import java
.util
.HashMap
;
25 import java
.util
.List
;
27 import java
.util
.Objects
;
28 import java
.util
.Optional
;
30 import java
.util
.UUID
;
31 import java
.util
.function
.Supplier
;
32 import java
.util
.stream
.Collectors
;
34 public class RecipientStore
implements RecipientIdCreator
, RecipientResolver
, RecipientTrustedResolver
, ContactsStore
, ProfileStore
{
36 private final static Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
37 private static final String TABLE_RECIPIENT
= "recipient";
38 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";
40 private final RecipientMergeHandler recipientMergeHandler
;
41 private final SelfAddressProvider selfAddressProvider
;
42 private final Database database
;
44 private final Object recipientsLock
= new Object();
45 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
47 public static void createSql(Connection connection
) throws SQLException
{
48 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
49 try (final var statement
= connection
.createStatement()) {
50 statement
.executeUpdate("""
51 CREATE TABLE recipient (
52 _id INTEGER PRIMARY KEY AUTOINCREMENT,
56 profile_key_credential BLOB,
62 expiration_time INTEGER NOT NULL DEFAULT 0,
63 blocked INTEGER NOT NULL DEFAULT FALSE,
64 archived INTEGER NOT NULL DEFAULT FALSE,
65 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
67 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
68 profile_given_name TEXT,
69 profile_family_name TEXT,
71 profile_about_emoji TEXT,
72 profile_avatar_url_path TEXT,
73 profile_mobile_coin_address BLOB,
74 profile_unidentified_access_mode TEXT,
75 profile_capabilities TEXT
81 public RecipientStore(
82 final RecipientMergeHandler recipientMergeHandler
,
83 final SelfAddressProvider selfAddressProvider
,
84 final Database database
86 this.recipientMergeHandler
= recipientMergeHandler
;
87 this.selfAddressProvider
= selfAddressProvider
;
88 this.database
= database
;
91 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
94 SELECT r.number, r.uuid
98 ).formatted(TABLE_RECIPIENT
);
99 try (final var connection
= database
.getConnection()) {
100 try (final var statement
= connection
.prepareStatement(sql
)) {
101 statement
.setLong(1, recipientId
.id());
102 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
104 } catch (SQLException e
) {
105 throw new RuntimeException("Failed read from recipient store", e
);
109 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
114 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
116 ).formatted(TABLE_RECIPIENT
);
117 try (final var connection
= database
.getConnection()) {
118 try (final var statement
= connection
.prepareStatement(sql
)) {
119 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
120 return result
.toList();
123 } catch (SQLException e
) {
124 throw new RuntimeException("Failed read from recipient store", e
);
129 public RecipientId
resolveRecipient(final long rawRecipientId
) {
136 ).formatted(TABLE_RECIPIENT
);
137 try (final var connection
= database
.getConnection()) {
138 try (final var statement
= connection
.prepareStatement(sql
)) {
139 statement
.setLong(1, rawRecipientId
);
140 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
142 } catch (SQLException e
) {
143 throw new RuntimeException("Failed read from recipient store", e
);
148 * Should only be used for recipientIds from the database.
149 * Where the foreign key relations ensure a valid recipientId.
152 public RecipientId
create(final long recipientId
) {
153 return new RecipientId(recipientId
, this);
156 public RecipientId
resolveRecipient(
157 final String number
, Supplier
<ACI
> aciSupplier
158 ) throws UnregisteredRecipientException
{
159 final Optional
<RecipientWithAddress
> byNumber
;
160 try (final var connection
= database
.getConnection()) {
161 byNumber
= findByNumber(connection
, number
);
162 } catch (SQLException e
) {
163 throw new RuntimeException("Failed read from recipient store", e
);
165 if (byNumber
.isEmpty() || byNumber
.get().address().uuid().isEmpty()) {
166 final var aci
= aciSupplier
.get();
168 throw new UnregisteredRecipientException(new RecipientAddress(null, number
));
171 return resolveRecipient(new RecipientAddress(aci
.uuid(), number
), false, false);
173 return byNumber
.get().id();
176 public RecipientId
resolveRecipient(RecipientAddress address
) {
177 return resolveRecipient(address
, false, false);
181 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
182 return resolveRecipient(address
, true, true);
185 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
186 return resolveRecipient(address
, true, false);
190 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
191 return resolveRecipient(new RecipientAddress(address
), true, false);
195 public void storeContact(RecipientId recipientId
, final Contact contact
) {
196 try (final var connection
= database
.getConnection()) {
197 storeContact(connection
, recipientId
, contact
);
198 } catch (SQLException e
) {
199 throw new RuntimeException("Failed update recipient store", e
);
204 public Contact
getContact(RecipientId recipientId
) {
205 try (final var connection
= database
.getConnection()) {
206 return getContact(connection
, recipientId
);
207 } catch (SQLException e
) {
208 throw new RuntimeException("Failed read from recipient store", e
);
213 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
216 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
218 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
220 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
221 try (final var connection
= database
.getConnection()) {
222 try (final var statement
= connection
.prepareStatement(sql
)) {
223 try (var result
= Utils
.executeQueryForStream(statement
,
224 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
225 getContactFromResultSet(resultSet
)))) {
226 return result
.toList();
229 } catch (SQLException e
) {
230 throw new RuntimeException("Failed read from recipient store", e
);
234 public List
<Recipient
> getRecipients(
235 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
237 final var sqlWhere
= new ArrayList
<String
>();
239 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
241 if (blocked
.isPresent()) {
242 sqlWhere
.add("r.blocked = ?");
244 if (!recipientIds
.isEmpty()) {
245 final var recipientIdsCommaSeparated
= recipientIds
.stream()
246 .map(recipientId
-> String
.valueOf(recipientId
.id()))
247 .collect(Collectors
.joining(","));
248 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
254 r.profile_key, r.profile_key_credential,
255 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
256 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
258 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
260 ).formatted(TABLE_RECIPIENT
, sqlWhere
.size() == 0 ?
"TRUE" : String
.join(" AND ", sqlWhere
));
261 try (final var connection
= database
.getConnection()) {
262 try (final var statement
= connection
.prepareStatement(sql
)) {
263 if (blocked
.isPresent()) {
264 statement
.setBoolean(1, blocked
.get());
266 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
267 return result
.filter(r
-> name
.isEmpty() || (
268 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
269 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).toList();
272 } catch (SQLException e
) {
273 throw new RuntimeException("Failed read from recipient store", e
);
278 public void deleteContact(RecipientId recipientId
) {
279 storeContact(recipientId
, null);
282 public void deleteRecipientData(RecipientId recipientId
) {
283 logger
.debug("Deleting recipient data for {}", recipientId
);
284 try (final var connection
= database
.getConnection()) {
285 connection
.setAutoCommit(false);
286 storeContact(connection
, recipientId
, null);
287 storeProfile(connection
, recipientId
, null);
288 storeProfileKey(connection
, recipientId
, null, false);
289 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
290 deleteRecipient(connection
, recipientId
);
292 } catch (SQLException e
) {
293 throw new RuntimeException("Failed update recipient store", e
);
298 public Profile
getProfile(final RecipientId recipientId
) {
299 try (final var connection
= database
.getConnection()) {
300 return getProfile(connection
, recipientId
);
301 } catch (SQLException e
) {
302 throw new RuntimeException("Failed read from recipient store", e
);
307 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
308 try (final var connection
= database
.getConnection()) {
309 return getProfileKey(connection
, recipientId
);
310 } catch (SQLException e
) {
311 throw new RuntimeException("Failed read from recipient store", e
);
316 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
317 try (final var connection
= database
.getConnection()) {
318 return getExpiringProfileKeyCredential(connection
, recipientId
);
319 } catch (SQLException e
) {
320 throw new RuntimeException("Failed read from recipient store", e
);
325 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
326 try (final var connection
= database
.getConnection()) {
327 storeProfile(connection
, recipientId
, profile
);
328 } catch (SQLException e
) {
329 throw new RuntimeException("Failed update recipient store", e
);
334 public void storeSelfProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
335 try (final var connection
= database
.getConnection()) {
336 storeProfileKey(connection
, recipientId
, profileKey
, false);
337 } catch (SQLException e
) {
338 throw new RuntimeException("Failed update recipient store", e
);
343 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
344 try (final var connection
= database
.getConnection()) {
345 storeProfileKey(connection
, recipientId
, profileKey
, true);
346 } catch (SQLException e
) {
347 throw new RuntimeException("Failed update recipient store", e
);
352 public void storeExpiringProfileKeyCredential(
353 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
355 try (final var connection
= database
.getConnection()) {
356 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
357 } catch (SQLException e
) {
358 throw new RuntimeException("Failed update recipient store", e
);
362 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
363 logger
.debug("Migrating legacy recipients to database");
364 long start
= System
.nanoTime();
367 INSERT INTO %s (_id, number, uuid)
370 ).formatted(TABLE_RECIPIENT
);
371 try (final var connection
= database
.getConnection()) {
372 connection
.setAutoCommit(false);
373 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
374 statement
.executeUpdate();
376 try (final var statement
= connection
.prepareStatement(sql
)) {
377 for (final var recipient
: recipients
.values()) {
378 statement
.setLong(1, recipient
.getRecipientId().id());
379 statement
.setString(2, recipient
.getAddress().number().orElse(null));
380 statement
.setBytes(3, recipient
.getAddress().uuid().map(UuidUtil
::toByteArray
).orElse(null));
381 statement
.executeUpdate();
384 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
386 for (final var recipient
: recipients
.values()) {
387 if (recipient
.getContact() != null) {
388 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
390 if (recipient
.getProfile() != null) {
391 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
393 if (recipient
.getProfileKey() != null) {
394 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
396 if (recipient
.getExpiringProfileKeyCredential() != null) {
397 storeExpiringProfileKeyCredential(connection
,
398 recipient
.getRecipientId(),
399 recipient
.getExpiringProfileKeyCredential());
403 } catch (SQLException e
) {
404 throw new RuntimeException("Failed update recipient store", e
);
406 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
409 long getActualRecipientId(long recipientId
) {
410 while (recipientsMerged
.containsKey(recipientId
)) {
411 final var newRecipientId
= recipientsMerged
.get(recipientId
);
412 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
413 recipientId
= newRecipientId
;
418 private void storeContact(
419 final Connection connection
, final RecipientId recipientId
, final Contact contact
420 ) throws SQLException
{
424 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
427 ).formatted(TABLE_RECIPIENT
);
428 try (final var statement
= connection
.prepareStatement(sql
)) {
429 statement
.setString(1, contact
== null ?
null : contact
.getGivenName());
430 statement
.setString(2, contact
== null ?
null : contact
.getFamilyName());
431 statement
.setInt(3, contact
== null ?
0 : contact
.getMessageExpirationTime());
432 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
433 statement
.setString(5, contact
== null ?
null : contact
.getColor());
434 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
435 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
436 statement
.setLong(8, recipientId
.id());
437 statement
.executeUpdate();
441 private void storeExpiringProfileKeyCredential(
442 final Connection connection
,
443 final RecipientId recipientId
,
444 final ExpiringProfileKeyCredential profileKeyCredential
445 ) throws SQLException
{
449 SET profile_key_credential = ?
452 ).formatted(TABLE_RECIPIENT
);
453 try (final var statement
= connection
.prepareStatement(sql
)) {
454 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
455 statement
.setLong(2, recipientId
.id());
456 statement
.executeUpdate();
460 private void storeProfile(
461 final Connection connection
, final RecipientId recipientId
, final Profile profile
462 ) throws SQLException
{
466 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 = ?
469 ).formatted(TABLE_RECIPIENT
);
470 try (final var statement
= connection
.prepareStatement(sql
)) {
471 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
472 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
473 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
474 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
475 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
476 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
477 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
478 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
479 statement
.setString(9,
482 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
483 statement
.setLong(10, recipientId
.id());
484 statement
.executeUpdate();
488 private void storeProfileKey(
489 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
490 ) throws SQLException
{
491 if (profileKey
!= null) {
492 final var recipientProfileKey
= getProfileKey(recipientId
);
493 if (profileKey
.equals(recipientProfileKey
)) {
494 final var recipientProfile
= getProfile(recipientId
);
495 if (recipientProfile
== null || (
496 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
497 && recipientProfile
.getUnidentifiedAccessMode()
498 != Profile
.UnidentifiedAccessMode
.DISABLED
508 SET profile_key = ?, profile_key_credential = NULL%s
511 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
512 try (final var statement
= connection
.prepareStatement(sql
)) {
513 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
514 statement
.setLong(2, recipientId
.id());
515 statement
.executeUpdate();
520 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
521 * Has no effect, if the address contains only a number or a uuid.
523 private RecipientId
resolveRecipient(RecipientAddress address
, boolean isHighTrust
, boolean isSelf
) {
524 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
525 synchronized (recipientsLock
) {
526 try (final var connection
= database
.getConnection()) {
527 connection
.setAutoCommit(false);
528 pair
= resolveRecipientLocked(connection
, address
, isHighTrust
, isSelf
);
530 } catch (SQLException e
) {
531 throw new RuntimeException("Failed update recipient store", e
);
535 if (pair
.second().isPresent()) {
536 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
537 try (final var connection
= database
.getConnection()) {
538 deleteRecipient(connection
, pair
.second().get());
539 } catch (SQLException e
) {
540 throw new RuntimeException("Failed update recipient store", e
);
546 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
547 Connection connection
, RecipientAddress address
, boolean isHighTrust
, boolean isSelf
548 ) throws SQLException
{
549 if (isHighTrust
&& !isSelf
) {
550 if (selfAddressProvider
.getSelfAddress().matches(address
)) {
554 final var byNumber
= address
.number().isEmpty()
555 ? Optional
.<RecipientWithAddress
>empty()
556 : findByNumber(connection
, address
.number().get());
557 final var byUuid
= address
.uuid().isEmpty()
558 ? Optional
.<RecipientWithAddress
>empty()
559 : findByUuid(connection
, address
.uuid().get());
561 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
562 logger
.debug("Got new recipient, both uuid and number are unknown");
564 if (isHighTrust
|| address
.uuid().isEmpty() || address
.number().isEmpty()) {
565 return new Pair
<>(addNewRecipient(connection
, address
), Optional
.empty());
568 return new Pair
<>(addNewRecipient(connection
, new RecipientAddress(address
.uuid().get())),
572 if (!isHighTrust
|| address
.uuid().isEmpty() || address
.number().isEmpty() || byNumber
.equals(byUuid
)) {
573 return new Pair
<>(byUuid
.or(() -> byNumber
).map(RecipientWithAddress
::id
).get(), Optional
.empty());
576 if (byNumber
.isEmpty()) {
577 logger
.debug("Got recipient {} existing with uuid, updating with high trust number", byUuid
.get().id());
578 updateRecipientAddress(connection
, byUuid
.get().id(), address
);
579 return new Pair
<>(byUuid
.get().id(), Optional
.empty());
582 final var byNumberRecipient
= byNumber
.get();
584 if (byUuid
.isEmpty()) {
585 if (byNumberRecipient
.address().uuid().isPresent()) {
587 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
588 byNumberRecipient
.id());
590 updateRecipientAddress(connection
,
591 byNumberRecipient
.id(),
592 new RecipientAddress(byNumberRecipient
.address().uuid().get()));
593 return new Pair
<>(addNewRecipient(connection
, address
), Optional
.empty());
596 logger
.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
597 byNumberRecipient
.id());
598 updateRecipientAddress(connection
, byNumberRecipient
.id(), address
);
599 return new Pair
<>(byNumberRecipient
.id(), Optional
.empty());
602 final var byUuidRecipient
= byUuid
.get();
604 if (byNumberRecipient
.address().uuid().isPresent()) {
606 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
607 byNumberRecipient
.id(),
608 byUuidRecipient
.id());
610 updateRecipientAddress(connection
,
611 byNumberRecipient
.id(),
612 new RecipientAddress(byNumberRecipient
.address().uuid().get()));
613 updateRecipientAddress(connection
, byUuidRecipient
.id(), address
);
614 return new Pair
<>(byUuidRecipient
.id(), Optional
.empty());
617 logger
.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
618 byNumberRecipient
.id(),
619 byUuidRecipient
.id());
620 // Create a fixed RecipientId that won't update its id after merge
621 final var toBeMergedRecipientId
= new RecipientId(byNumberRecipient
.id().id(), null);
622 mergeRecipientsLocked(connection
, byUuidRecipient
.id(), toBeMergedRecipientId
);
623 removeRecipientAddress(connection
, toBeMergedRecipientId
);
624 updateRecipientAddress(connection
, byUuidRecipient
.id(), address
);
625 return new Pair
<>(byUuidRecipient
.id(), Optional
.of(toBeMergedRecipientId
));
628 private RecipientId
addNewRecipient(
629 final Connection connection
, final RecipientAddress address
630 ) throws SQLException
{
633 INSERT INTO %s (number, uuid)
636 ).formatted(TABLE_RECIPIENT
);
637 try (final var statement
= connection
.prepareStatement(sql
)) {
638 statement
.setString(1, address
.number().orElse(null));
639 statement
.setBytes(2, address
.uuid().map(UuidUtil
::toByteArray
).orElse(null));
640 statement
.executeUpdate();
641 final var generatedKeys
= statement
.getGeneratedKeys();
642 if (generatedKeys
.next()) {
643 final var recipientId
= new RecipientId(generatedKeys
.getLong(1), this);
644 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
647 throw new RuntimeException("Failed to add new recipient to database");
652 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
656 SET number = NULL, uuid = NULL
659 ).formatted(TABLE_RECIPIENT
);
660 try (final var statement
= connection
.prepareStatement(sql
)) {
661 statement
.setLong(1, recipientId
.id());
662 statement
.executeUpdate();
666 private void updateRecipientAddress(
667 Connection connection
, RecipientId recipientId
, final RecipientAddress address
668 ) throws SQLException
{
672 SET number = ?, uuid = ?
675 ).formatted(TABLE_RECIPIENT
);
676 try (final var statement
= connection
.prepareStatement(sql
)) {
677 statement
.setString(1, address
.number().orElse(null));
678 statement
.setBytes(2, address
.uuid().map(UuidUtil
::toByteArray
).orElse(null));
679 statement
.setLong(3, recipientId
.id());
680 statement
.executeUpdate();
684 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
690 ).formatted(TABLE_RECIPIENT
);
691 try (final var statement
= connection
.prepareStatement(sql
)) {
692 statement
.setLong(1, recipientId
.id());
693 statement
.executeUpdate();
697 private void mergeRecipientsLocked(
698 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
699 ) throws SQLException
{
700 final var contact
= getContact(connection
, recipientId
);
701 if (contact
== null) {
702 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
703 storeContact(connection
, recipientId
, toBeMergedContact
);
706 final var profileKey
= getProfileKey(connection
, recipientId
);
707 if (profileKey
== null) {
708 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
709 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
712 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
713 if (profileKeyCredential
== null) {
714 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
715 toBeMergedRecipientId
);
716 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
719 final var profile
= getProfile(connection
, recipientId
);
720 if (profile
== null) {
721 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
722 storeProfile(connection
, recipientId
, toBeMergedProfile
);
725 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
728 private Optional
<RecipientWithAddress
> findByNumber(
729 final Connection connection
, final String number
730 ) throws SQLException
{
732 SELECT r._id, r.number, r.uuid
735 """.formatted(TABLE_RECIPIENT
);
736 try (final var statement
= connection
.prepareStatement(sql
)) {
737 statement
.setString(1, number
);
738 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
742 private Optional
<RecipientWithAddress
> findByUuid(
743 final Connection connection
, final UUID uuid
744 ) throws SQLException
{
746 SELECT r._id, r.number, r.uuid
749 """.formatted(TABLE_RECIPIENT
);
750 try (final var statement
= connection
.prepareStatement(sql
)) {
751 statement
.setBytes(1, UuidUtil
.toByteArray(uuid
));
752 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
756 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
759 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
761 WHERE r._id = ? AND (%s)
763 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
764 try (final var statement
= connection
.prepareStatement(sql
)) {
765 statement
.setLong(1, recipientId
.id());
766 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
770 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
777 ).formatted(TABLE_RECIPIENT
);
778 try (final var statement
= connection
.prepareStatement(sql
)) {
779 statement
.setLong(1, recipientId
.id());
780 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
784 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
785 final Connection connection
, final RecipientId recipientId
786 ) throws SQLException
{
789 SELECT r.profile_key_credential
793 ).formatted(TABLE_RECIPIENT
);
794 try (final var statement
= connection
.prepareStatement(sql
)) {
795 statement
.setLong(1, recipientId
.id());
796 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
801 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
804 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
806 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
808 ).formatted(TABLE_RECIPIENT
);
809 try (final var statement
= connection
.prepareStatement(sql
)) {
810 statement
.setLong(1, recipientId
.id());
811 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
815 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
816 final var uuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
817 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
818 return new RecipientAddress(uuid
, number
);
821 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
822 return new RecipientId(resultSet
.getLong("_id"), this);
825 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
826 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
827 getRecipientAddressFromResultSet(resultSet
));
830 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
831 return new Recipient(getRecipientIdFromResultSet(resultSet
),
832 getRecipientAddressFromResultSet(resultSet
),
833 getContactFromResultSet(resultSet
),
834 getProfileKeyFromResultSet(resultSet
),
835 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
836 getProfileFromResultSet(resultSet
));
839 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
840 return new Contact(resultSet
.getString("given_name"),
841 resultSet
.getString("family_name"),
842 resultSet
.getString("color"),
843 resultSet
.getInt("expiration_time"),
844 resultSet
.getBoolean("blocked"),
845 resultSet
.getBoolean("archived"),
846 resultSet
.getBoolean("profile_sharing"));
849 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
850 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
851 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
852 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
853 resultSet
.getString("profile_given_name"),
854 resultSet
.getString("profile_family_name"),
855 resultSet
.getString("profile_about"),
856 resultSet
.getString("profile_about_emoji"),
857 resultSet
.getString("profile_avatar_url_path"),
858 resultSet
.getBytes("profile_mobile_coin_address"),
859 profileUnidentifiedAccessMode
== null
860 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
861 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
862 profileCapabilities
== null
864 : Arrays
.stream(profileCapabilities
.split(","))
865 .map(Profile
.Capability
::valueOfOrNull
)
866 .filter(Objects
::nonNull
)
867 .collect(Collectors
.toSet()));
870 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
871 final var profileKey
= resultSet
.getBytes("profile_key");
873 if (profileKey
== null) {
877 return new ProfileKey(profileKey
);
878 } catch (InvalidInputException ignored
) {
883 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
884 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
886 if (profileKeyCredential
== null) {
890 return new ExpiringProfileKeyCredential(profileKeyCredential
);
891 } catch (Throwable ignored
) {
896 public interface RecipientMergeHandler
{
898 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);
901 private record RecipientWithAddress(RecipientId id
, RecipientAddress address
) {}