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
.ServiceId
;
16 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
17 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
19 import java
.sql
.Connection
;
20 import java
.sql
.ResultSet
;
21 import java
.sql
.SQLException
;
22 import java
.util
.ArrayList
;
23 import java
.util
.Arrays
;
24 import java
.util
.Collection
;
25 import java
.util
.HashMap
;
26 import java
.util
.List
;
28 import java
.util
.Objects
;
29 import java
.util
.Optional
;
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().serviceId().isEmpty()) {
166 final var aci
= aciSupplier
.get();
168 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null, number
));
171 return resolveRecipient(new RecipientAddress(aci
, 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
);
277 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
280 SELECT r.uuid, r.profile_key
282 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
284 ).formatted(TABLE_RECIPIENT
);
285 try (final var connection
= database
.getConnection()) {
286 try (final var statement
= connection
.prepareStatement(sql
)) {
287 return Utils
.executeQueryForStream(statement
, resultSet
-> {
288 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
289 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
290 return new Pair
<>(serviceId
, profileKey
);
291 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
293 } catch (SQLException e
) {
294 throw new RuntimeException("Failed read from recipient store", e
);
299 public void deleteContact(RecipientId recipientId
) {
300 storeContact(recipientId
, null);
303 public void deleteRecipientData(RecipientId recipientId
) {
304 logger
.debug("Deleting recipient data for {}", recipientId
);
305 try (final var connection
= database
.getConnection()) {
306 connection
.setAutoCommit(false);
307 storeContact(connection
, recipientId
, null);
308 storeProfile(connection
, recipientId
, null);
309 storeProfileKey(connection
, recipientId
, null, false);
310 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
311 deleteRecipient(connection
, recipientId
);
313 } catch (SQLException e
) {
314 throw new RuntimeException("Failed update recipient store", e
);
319 public Profile
getProfile(final RecipientId recipientId
) {
320 try (final var connection
= database
.getConnection()) {
321 return getProfile(connection
, recipientId
);
322 } catch (SQLException e
) {
323 throw new RuntimeException("Failed read from recipient store", e
);
328 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
329 try (final var connection
= database
.getConnection()) {
330 return getProfileKey(connection
, recipientId
);
331 } catch (SQLException e
) {
332 throw new RuntimeException("Failed read from recipient store", e
);
337 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
338 try (final var connection
= database
.getConnection()) {
339 return getExpiringProfileKeyCredential(connection
, recipientId
);
340 } catch (SQLException e
) {
341 throw new RuntimeException("Failed read from recipient store", e
);
346 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
347 try (final var connection
= database
.getConnection()) {
348 storeProfile(connection
, recipientId
, profile
);
349 } catch (SQLException e
) {
350 throw new RuntimeException("Failed update recipient store", e
);
355 public void storeSelfProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
356 try (final var connection
= database
.getConnection()) {
357 storeProfileKey(connection
, recipientId
, profileKey
, false);
358 } catch (SQLException e
) {
359 throw new RuntimeException("Failed update recipient store", e
);
364 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
365 try (final var connection
= database
.getConnection()) {
366 storeProfileKey(connection
, recipientId
, profileKey
, true);
367 } catch (SQLException e
) {
368 throw new RuntimeException("Failed update recipient store", e
);
373 public void storeExpiringProfileKeyCredential(
374 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
376 try (final var connection
= database
.getConnection()) {
377 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
378 } catch (SQLException e
) {
379 throw new RuntimeException("Failed update recipient store", e
);
383 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
384 logger
.debug("Migrating legacy recipients to database");
385 long start
= System
.nanoTime();
388 INSERT INTO %s (_id, number, uuid)
391 ).formatted(TABLE_RECIPIENT
);
392 try (final var connection
= database
.getConnection()) {
393 connection
.setAutoCommit(false);
394 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
395 statement
.executeUpdate();
397 try (final var statement
= connection
.prepareStatement(sql
)) {
398 for (final var recipient
: recipients
.values()) {
399 statement
.setLong(1, recipient
.getRecipientId().id());
400 statement
.setString(2, recipient
.getAddress().number().orElse(null));
401 statement
.setBytes(3,
402 recipient
.getAddress()
404 .map(ServiceId
::uuid
)
405 .map(UuidUtil
::toByteArray
)
407 statement
.executeUpdate();
410 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
412 for (final var recipient
: recipients
.values()) {
413 if (recipient
.getContact() != null) {
414 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
416 if (recipient
.getProfile() != null) {
417 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
419 if (recipient
.getProfileKey() != null) {
420 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
422 if (recipient
.getExpiringProfileKeyCredential() != null) {
423 storeExpiringProfileKeyCredential(connection
,
424 recipient
.getRecipientId(),
425 recipient
.getExpiringProfileKeyCredential());
429 } catch (SQLException e
) {
430 throw new RuntimeException("Failed update recipient store", e
);
432 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
435 long getActualRecipientId(long recipientId
) {
436 while (recipientsMerged
.containsKey(recipientId
)) {
437 final var newRecipientId
= recipientsMerged
.get(recipientId
);
438 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
439 recipientId
= newRecipientId
;
444 private void storeContact(
445 final Connection connection
, final RecipientId recipientId
, final Contact contact
446 ) throws SQLException
{
450 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
453 ).formatted(TABLE_RECIPIENT
);
454 try (final var statement
= connection
.prepareStatement(sql
)) {
455 statement
.setString(1, contact
== null ?
null : contact
.getGivenName());
456 statement
.setString(2, contact
== null ?
null : contact
.getFamilyName());
457 statement
.setInt(3, contact
== null ?
0 : contact
.getMessageExpirationTime());
458 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
459 statement
.setString(5, contact
== null ?
null : contact
.getColor());
460 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
461 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
462 statement
.setLong(8, recipientId
.id());
463 statement
.executeUpdate();
467 private void storeExpiringProfileKeyCredential(
468 final Connection connection
,
469 final RecipientId recipientId
,
470 final ExpiringProfileKeyCredential profileKeyCredential
471 ) throws SQLException
{
475 SET profile_key_credential = ?
478 ).formatted(TABLE_RECIPIENT
);
479 try (final var statement
= connection
.prepareStatement(sql
)) {
480 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
481 statement
.setLong(2, recipientId
.id());
482 statement
.executeUpdate();
486 private void storeProfile(
487 final Connection connection
, final RecipientId recipientId
, final Profile profile
488 ) throws SQLException
{
492 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 = ?
495 ).formatted(TABLE_RECIPIENT
);
496 try (final var statement
= connection
.prepareStatement(sql
)) {
497 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
498 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
499 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
500 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
501 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
502 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
503 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
504 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
505 statement
.setString(9,
508 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
509 statement
.setLong(10, recipientId
.id());
510 statement
.executeUpdate();
514 private void storeProfileKey(
515 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
516 ) throws SQLException
{
517 if (profileKey
!= null) {
518 final var recipientProfileKey
= getProfileKey(recipientId
);
519 if (profileKey
.equals(recipientProfileKey
)) {
520 final var recipientProfile
= getProfile(recipientId
);
521 if (recipientProfile
== null || (
522 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
523 && recipientProfile
.getUnidentifiedAccessMode()
524 != Profile
.UnidentifiedAccessMode
.DISABLED
534 SET profile_key = ?, profile_key_credential = NULL%s
537 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
538 try (final var statement
= connection
.prepareStatement(sql
)) {
539 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
540 statement
.setLong(2, recipientId
.id());
541 statement
.executeUpdate();
546 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
547 * Has no effect, if the address contains only a number or a uuid.
549 private RecipientId
resolveRecipient(RecipientAddress address
, boolean isHighTrust
, boolean isSelf
) {
550 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
551 synchronized (recipientsLock
) {
552 try (final var connection
= database
.getConnection()) {
553 connection
.setAutoCommit(false);
554 pair
= resolveRecipientLocked(connection
, address
, isHighTrust
, isSelf
);
556 } catch (SQLException e
) {
557 throw new RuntimeException("Failed update recipient store", e
);
561 if (pair
.second().isPresent()) {
562 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
563 try (final var connection
= database
.getConnection()) {
564 deleteRecipient(connection
, pair
.second().get());
565 } catch (SQLException e
) {
566 throw new RuntimeException("Failed update recipient store", e
);
572 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
573 Connection connection
, RecipientAddress address
, boolean isHighTrust
, boolean isSelf
574 ) throws SQLException
{
575 if (isHighTrust
&& !isSelf
) {
576 if (selfAddressProvider
.getSelfAddress().matches(address
)) {
580 final var byNumber
= address
.number().isEmpty()
581 ? Optional
.<RecipientWithAddress
>empty()
582 : findByNumber(connection
, address
.number().get());
583 final var byUuid
= address
.serviceId().isEmpty()
584 ? Optional
.<RecipientWithAddress
>empty()
585 : findByServiceId(connection
, address
.serviceId().get());
587 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
588 logger
.debug("Got new recipient, both uuid and number are unknown");
590 if (isHighTrust
|| address
.serviceId().isEmpty() || address
.number().isEmpty()) {
591 return new Pair
<>(addNewRecipient(connection
, address
), Optional
.empty());
594 return new Pair
<>(addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get())),
598 if (!isHighTrust
|| address
.serviceId().isEmpty() || address
.number().isEmpty() || byNumber
.equals(byUuid
)) {
599 return new Pair
<>(byUuid
.or(() -> byNumber
).map(RecipientWithAddress
::id
).get(), Optional
.empty());
602 if (byNumber
.isEmpty()) {
603 logger
.debug("Got recipient {} existing with uuid, updating with high trust number", byUuid
.get().id());
604 updateRecipientAddress(connection
, byUuid
.get().id(), address
);
605 return new Pair
<>(byUuid
.get().id(), Optional
.empty());
608 final var byNumberRecipient
= byNumber
.get();
610 if (byUuid
.isEmpty()) {
611 if (byNumberRecipient
.address().serviceId().isPresent()) {
613 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
614 byNumberRecipient
.id());
616 updateRecipientAddress(connection
,
617 byNumberRecipient
.id(),
618 new RecipientAddress(byNumberRecipient
.address().serviceId().get()));
619 return new Pair
<>(addNewRecipient(connection
, address
), Optional
.empty());
622 logger
.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
623 byNumberRecipient
.id());
624 updateRecipientAddress(connection
, byNumberRecipient
.id(), address
);
625 return new Pair
<>(byNumberRecipient
.id(), Optional
.empty());
628 final var byUuidRecipient
= byUuid
.get();
630 if (byNumberRecipient
.address().serviceId().isPresent()) {
632 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
633 byNumberRecipient
.id(),
634 byUuidRecipient
.id());
636 updateRecipientAddress(connection
,
637 byNumberRecipient
.id(),
638 new RecipientAddress(byNumberRecipient
.address().serviceId().get()));
639 updateRecipientAddress(connection
, byUuidRecipient
.id(), address
);
640 return new Pair
<>(byUuidRecipient
.id(), Optional
.empty());
643 logger
.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
644 byNumberRecipient
.id(),
645 byUuidRecipient
.id());
646 // Create a fixed RecipientId that won't update its id after merge
647 final var toBeMergedRecipientId
= new RecipientId(byNumberRecipient
.id().id(), null);
648 mergeRecipientsLocked(connection
, byUuidRecipient
.id(), toBeMergedRecipientId
);
649 removeRecipientAddress(connection
, toBeMergedRecipientId
);
650 updateRecipientAddress(connection
, byUuidRecipient
.id(), address
);
651 return new Pair
<>(byUuidRecipient
.id(), Optional
.of(toBeMergedRecipientId
));
654 private RecipientId
addNewRecipient(
655 final Connection connection
, final RecipientAddress address
656 ) throws SQLException
{
659 INSERT INTO %s (number, uuid)
662 ).formatted(TABLE_RECIPIENT
);
663 try (final var statement
= connection
.prepareStatement(sql
)) {
664 statement
.setString(1, address
.number().orElse(null));
665 statement
.setBytes(2, address
.serviceId().map(ServiceId
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
666 statement
.executeUpdate();
667 final var generatedKeys
= statement
.getGeneratedKeys();
668 if (generatedKeys
.next()) {
669 final var recipientId
= new RecipientId(generatedKeys
.getLong(1), this);
670 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
673 throw new RuntimeException("Failed to add new recipient to database");
678 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
682 SET number = NULL, uuid = NULL
685 ).formatted(TABLE_RECIPIENT
);
686 try (final var statement
= connection
.prepareStatement(sql
)) {
687 statement
.setLong(1, recipientId
.id());
688 statement
.executeUpdate();
692 private void updateRecipientAddress(
693 Connection connection
, RecipientId recipientId
, final RecipientAddress address
694 ) throws SQLException
{
698 SET number = ?, uuid = ?
701 ).formatted(TABLE_RECIPIENT
);
702 try (final var statement
= connection
.prepareStatement(sql
)) {
703 statement
.setString(1, address
.number().orElse(null));
704 statement
.setBytes(2, address
.serviceId().map(ServiceId
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
705 statement
.setLong(3, recipientId
.id());
706 statement
.executeUpdate();
710 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
716 ).formatted(TABLE_RECIPIENT
);
717 try (final var statement
= connection
.prepareStatement(sql
)) {
718 statement
.setLong(1, recipientId
.id());
719 statement
.executeUpdate();
723 private void mergeRecipientsLocked(
724 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
725 ) throws SQLException
{
726 final var contact
= getContact(connection
, recipientId
);
727 if (contact
== null) {
728 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
729 storeContact(connection
, recipientId
, toBeMergedContact
);
732 final var profileKey
= getProfileKey(connection
, recipientId
);
733 if (profileKey
== null) {
734 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
735 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
738 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
739 if (profileKeyCredential
== null) {
740 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
741 toBeMergedRecipientId
);
742 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
745 final var profile
= getProfile(connection
, recipientId
);
746 if (profile
== null) {
747 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
748 storeProfile(connection
, recipientId
, toBeMergedProfile
);
751 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
754 private Optional
<RecipientWithAddress
> findByNumber(
755 final Connection connection
, final String number
756 ) throws SQLException
{
758 SELECT r._id, r.number, r.uuid
761 """.formatted(TABLE_RECIPIENT
);
762 try (final var statement
= connection
.prepareStatement(sql
)) {
763 statement
.setString(1, number
);
764 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
768 private Optional
<RecipientWithAddress
> findByServiceId(
769 final Connection connection
, final ServiceId serviceId
770 ) throws SQLException
{
772 SELECT r._id, r.number, r.uuid
775 """.formatted(TABLE_RECIPIENT
);
776 try (final var statement
= connection
.prepareStatement(sql
)) {
777 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.uuid()));
778 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
782 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
785 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
787 WHERE r._id = ? AND (%s)
789 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
790 try (final var statement
= connection
.prepareStatement(sql
)) {
791 statement
.setLong(1, recipientId
.id());
792 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
796 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
803 ).formatted(TABLE_RECIPIENT
);
804 try (final var statement
= connection
.prepareStatement(sql
)) {
805 statement
.setLong(1, recipientId
.id());
806 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
810 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
811 final Connection connection
, final RecipientId recipientId
812 ) throws SQLException
{
815 SELECT r.profile_key_credential
819 ).formatted(TABLE_RECIPIENT
);
820 try (final var statement
= connection
.prepareStatement(sql
)) {
821 statement
.setLong(1, recipientId
.id());
822 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
827 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
830 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
832 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
834 ).formatted(TABLE_RECIPIENT
);
835 try (final var statement
= connection
.prepareStatement(sql
)) {
836 statement
.setLong(1, recipientId
.id());
837 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
841 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
842 final var serviceId
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(ServiceId
::parseOrNull
);
843 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
844 return new RecipientAddress(serviceId
, Optional
.empty(), number
);
847 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
848 return new RecipientId(resultSet
.getLong("_id"), this);
851 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
852 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
853 getRecipientAddressFromResultSet(resultSet
));
856 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
857 return new Recipient(getRecipientIdFromResultSet(resultSet
),
858 getRecipientAddressFromResultSet(resultSet
),
859 getContactFromResultSet(resultSet
),
860 getProfileKeyFromResultSet(resultSet
),
861 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
862 getProfileFromResultSet(resultSet
));
865 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
866 return new Contact(resultSet
.getString("given_name"),
867 resultSet
.getString("family_name"),
868 resultSet
.getString("color"),
869 resultSet
.getInt("expiration_time"),
870 resultSet
.getBoolean("blocked"),
871 resultSet
.getBoolean("archived"),
872 resultSet
.getBoolean("profile_sharing"));
875 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
876 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
877 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
878 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
879 resultSet
.getString("profile_given_name"),
880 resultSet
.getString("profile_family_name"),
881 resultSet
.getString("profile_about"),
882 resultSet
.getString("profile_about_emoji"),
883 resultSet
.getString("profile_avatar_url_path"),
884 resultSet
.getBytes("profile_mobile_coin_address"),
885 profileUnidentifiedAccessMode
== null
886 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
887 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
888 profileCapabilities
== null
890 : Arrays
.stream(profileCapabilities
.split(","))
891 .map(Profile
.Capability
::valueOfOrNull
)
892 .filter(Objects
::nonNull
)
893 .collect(Collectors
.toSet()));
896 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
897 final var profileKey
= resultSet
.getBytes("profile_key");
899 if (profileKey
== null) {
903 return new ProfileKey(profileKey
);
904 } catch (InvalidInputException ignored
) {
909 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
910 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
912 if (profileKeyCredential
== null) {
916 return new ExpiringProfileKeyCredential(profileKeyCredential
);
917 } catch (Throwable ignored
) {
922 public interface RecipientMergeHandler
{
924 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);
927 private record RecipientWithAddress(RecipientId id
, RecipientAddress address
) {}