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
.UUID
;
32 import java
.util
.function
.Supplier
;
33 import java
.util
.stream
.Collectors
;
35 public class RecipientStore
implements RecipientIdCreator
, RecipientResolver
, RecipientTrustedResolver
, ContactsStore
, ProfileStore
{
37 private final static Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
38 private static final String TABLE_RECIPIENT
= "recipient";
39 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";
41 private final RecipientMergeHandler recipientMergeHandler
;
42 private final SelfAddressProvider selfAddressProvider
;
43 private final Database database
;
45 private final Object recipientsLock
= new Object();
46 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
48 public static void createSql(Connection connection
) throws SQLException
{
49 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
50 try (final var statement
= connection
.createStatement()) {
51 statement
.executeUpdate("""
52 CREATE TABLE recipient (
53 _id INTEGER PRIMARY KEY AUTOINCREMENT,
57 profile_key_credential BLOB,
63 expiration_time INTEGER NOT NULL DEFAULT 0,
64 blocked INTEGER NOT NULL DEFAULT FALSE,
65 archived INTEGER NOT NULL DEFAULT FALSE,
66 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
68 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
69 profile_given_name TEXT,
70 profile_family_name TEXT,
72 profile_about_emoji TEXT,
73 profile_avatar_url_path TEXT,
74 profile_mobile_coin_address BLOB,
75 profile_unidentified_access_mode TEXT,
76 profile_capabilities TEXT
82 public RecipientStore(
83 final RecipientMergeHandler recipientMergeHandler
,
84 final SelfAddressProvider selfAddressProvider
,
85 final Database database
87 this.recipientMergeHandler
= recipientMergeHandler
;
88 this.selfAddressProvider
= selfAddressProvider
;
89 this.database
= database
;
92 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
95 SELECT r.number, r.uuid
99 ).formatted(TABLE_RECIPIENT
);
100 try (final var connection
= database
.getConnection()) {
101 try (final var statement
= connection
.prepareStatement(sql
)) {
102 statement
.setLong(1, recipientId
.id());
103 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
105 } catch (SQLException e
) {
106 throw new RuntimeException("Failed read from recipient store", e
);
110 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
115 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
117 ).formatted(TABLE_RECIPIENT
);
118 try (final var connection
= database
.getConnection()) {
119 try (final var statement
= connection
.prepareStatement(sql
)) {
120 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
121 return result
.toList();
124 } catch (SQLException e
) {
125 throw new RuntimeException("Failed read from recipient store", e
);
130 public RecipientId
resolveRecipient(final long rawRecipientId
) {
137 ).formatted(TABLE_RECIPIENT
);
138 try (final var connection
= database
.getConnection()) {
139 try (final var statement
= connection
.prepareStatement(sql
)) {
140 statement
.setLong(1, rawRecipientId
);
141 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
143 } catch (SQLException e
) {
144 throw new RuntimeException("Failed read from recipient store", e
);
149 * Should only be used for recipientIds from the database.
150 * Where the foreign key relations ensure a valid recipientId.
153 public RecipientId
create(final long recipientId
) {
154 return new RecipientId(recipientId
, this);
157 public RecipientId
resolveRecipient(
158 final String number
, Supplier
<ACI
> aciSupplier
159 ) throws UnregisteredRecipientException
{
160 final Optional
<RecipientWithAddress
> byNumber
;
161 try (final var connection
= database
.getConnection()) {
162 byNumber
= findByNumber(connection
, number
);
163 } catch (SQLException e
) {
164 throw new RuntimeException("Failed read from recipient store", e
);
166 if (byNumber
.isEmpty() || byNumber
.get().address().uuid().isEmpty()) {
167 final var aci
= aciSupplier
.get();
169 throw new UnregisteredRecipientException(new RecipientAddress(null, number
));
172 return resolveRecipient(new RecipientAddress(aci
.uuid(), number
), false, false);
174 return byNumber
.get().id();
177 public RecipientId
resolveRecipient(RecipientAddress address
) {
178 return resolveRecipient(address
, false, false);
182 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
183 return resolveRecipient(address
, true, true);
186 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
187 return resolveRecipient(address
, true, false);
191 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
192 return resolveRecipient(new RecipientAddress(address
), true, false);
196 public void storeContact(RecipientId recipientId
, final Contact contact
) {
197 try (final var connection
= database
.getConnection()) {
198 storeContact(connection
, recipientId
, contact
);
199 } catch (SQLException e
) {
200 throw new RuntimeException("Failed update recipient store", e
);
205 public Contact
getContact(RecipientId recipientId
) {
206 try (final var connection
= database
.getConnection()) {
207 return getContact(connection
, recipientId
);
208 } catch (SQLException e
) {
209 throw new RuntimeException("Failed read from recipient store", e
);
214 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
217 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
219 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
221 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
222 try (final var connection
= database
.getConnection()) {
223 try (final var statement
= connection
.prepareStatement(sql
)) {
224 try (var result
= Utils
.executeQueryForStream(statement
,
225 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
226 getContactFromResultSet(resultSet
)))) {
227 return result
.toList();
230 } catch (SQLException e
) {
231 throw new RuntimeException("Failed read from recipient store", e
);
235 public List
<Recipient
> getRecipients(
236 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
238 final var sqlWhere
= new ArrayList
<String
>();
240 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
242 if (blocked
.isPresent()) {
243 sqlWhere
.add("r.blocked = ?");
245 if (!recipientIds
.isEmpty()) {
246 final var recipientIdsCommaSeparated
= recipientIds
.stream()
247 .map(recipientId
-> String
.valueOf(recipientId
.id()))
248 .collect(Collectors
.joining(","));
249 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
255 r.profile_key, r.profile_key_credential,
256 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
257 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
259 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
261 ).formatted(TABLE_RECIPIENT
, sqlWhere
.size() == 0 ?
"TRUE" : String
.join(" AND ", sqlWhere
));
262 try (final var connection
= database
.getConnection()) {
263 try (final var statement
= connection
.prepareStatement(sql
)) {
264 if (blocked
.isPresent()) {
265 statement
.setBoolean(1, blocked
.get());
267 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
268 return result
.filter(r
-> name
.isEmpty() || (
269 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
270 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).toList();
273 } catch (SQLException e
) {
274 throw new RuntimeException("Failed read from recipient store", e
);
278 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
281 SELECT r.uuid, r.profile_key
283 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
285 ).formatted(TABLE_RECIPIENT
);
286 try (final var connection
= database
.getConnection()) {
287 try (final var statement
= connection
.prepareStatement(sql
)) {
288 return Utils
.executeQueryForStream(statement
, resultSet
-> {
289 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
290 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
291 return new Pair
<>(serviceId
, profileKey
);
292 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
294 } catch (SQLException e
) {
295 throw new RuntimeException("Failed read from recipient store", e
);
300 public void deleteContact(RecipientId recipientId
) {
301 storeContact(recipientId
, null);
304 public void deleteRecipientData(RecipientId recipientId
) {
305 logger
.debug("Deleting recipient data for {}", recipientId
);
306 try (final var connection
= database
.getConnection()) {
307 connection
.setAutoCommit(false);
308 storeContact(connection
, recipientId
, null);
309 storeProfile(connection
, recipientId
, null);
310 storeProfileKey(connection
, recipientId
, null, false);
311 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
312 deleteRecipient(connection
, recipientId
);
314 } catch (SQLException e
) {
315 throw new RuntimeException("Failed update recipient store", e
);
320 public Profile
getProfile(final RecipientId recipientId
) {
321 try (final var connection
= database
.getConnection()) {
322 return getProfile(connection
, recipientId
);
323 } catch (SQLException e
) {
324 throw new RuntimeException("Failed read from recipient store", e
);
329 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
330 try (final var connection
= database
.getConnection()) {
331 return getProfileKey(connection
, recipientId
);
332 } catch (SQLException e
) {
333 throw new RuntimeException("Failed read from recipient store", e
);
338 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
339 try (final var connection
= database
.getConnection()) {
340 return getExpiringProfileKeyCredential(connection
, recipientId
);
341 } catch (SQLException e
) {
342 throw new RuntimeException("Failed read from recipient store", e
);
347 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
348 try (final var connection
= database
.getConnection()) {
349 storeProfile(connection
, recipientId
, profile
);
350 } catch (SQLException e
) {
351 throw new RuntimeException("Failed update recipient store", e
);
356 public void storeSelfProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
357 try (final var connection
= database
.getConnection()) {
358 storeProfileKey(connection
, recipientId
, profileKey
, false);
359 } catch (SQLException e
) {
360 throw new RuntimeException("Failed update recipient store", e
);
365 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
366 try (final var connection
= database
.getConnection()) {
367 storeProfileKey(connection
, recipientId
, profileKey
, true);
368 } catch (SQLException e
) {
369 throw new RuntimeException("Failed update recipient store", e
);
374 public void storeExpiringProfileKeyCredential(
375 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
377 try (final var connection
= database
.getConnection()) {
378 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
379 } catch (SQLException e
) {
380 throw new RuntimeException("Failed update recipient store", e
);
384 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
385 logger
.debug("Migrating legacy recipients to database");
386 long start
= System
.nanoTime();
389 INSERT INTO %s (_id, number, uuid)
392 ).formatted(TABLE_RECIPIENT
);
393 try (final var connection
= database
.getConnection()) {
394 connection
.setAutoCommit(false);
395 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
396 statement
.executeUpdate();
398 try (final var statement
= connection
.prepareStatement(sql
)) {
399 for (final var recipient
: recipients
.values()) {
400 statement
.setLong(1, recipient
.getRecipientId().id());
401 statement
.setString(2, recipient
.getAddress().number().orElse(null));
402 statement
.setBytes(3, recipient
.getAddress().uuid().map(UuidUtil
::toByteArray
).orElse(null));
403 statement
.executeUpdate();
406 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
408 for (final var recipient
: recipients
.values()) {
409 if (recipient
.getContact() != null) {
410 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
412 if (recipient
.getProfile() != null) {
413 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
415 if (recipient
.getProfileKey() != null) {
416 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
418 if (recipient
.getExpiringProfileKeyCredential() != null) {
419 storeExpiringProfileKeyCredential(connection
,
420 recipient
.getRecipientId(),
421 recipient
.getExpiringProfileKeyCredential());
425 } catch (SQLException e
) {
426 throw new RuntimeException("Failed update recipient store", e
);
428 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
431 long getActualRecipientId(long recipientId
) {
432 while (recipientsMerged
.containsKey(recipientId
)) {
433 final var newRecipientId
= recipientsMerged
.get(recipientId
);
434 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
435 recipientId
= newRecipientId
;
440 private void storeContact(
441 final Connection connection
, final RecipientId recipientId
, final Contact contact
442 ) throws SQLException
{
446 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
449 ).formatted(TABLE_RECIPIENT
);
450 try (final var statement
= connection
.prepareStatement(sql
)) {
451 statement
.setString(1, contact
== null ?
null : contact
.getGivenName());
452 statement
.setString(2, contact
== null ?
null : contact
.getFamilyName());
453 statement
.setInt(3, contact
== null ?
0 : contact
.getMessageExpirationTime());
454 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
455 statement
.setString(5, contact
== null ?
null : contact
.getColor());
456 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
457 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
458 statement
.setLong(8, recipientId
.id());
459 statement
.executeUpdate();
463 private void storeExpiringProfileKeyCredential(
464 final Connection connection
,
465 final RecipientId recipientId
,
466 final ExpiringProfileKeyCredential profileKeyCredential
467 ) throws SQLException
{
471 SET profile_key_credential = ?
474 ).formatted(TABLE_RECIPIENT
);
475 try (final var statement
= connection
.prepareStatement(sql
)) {
476 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
477 statement
.setLong(2, recipientId
.id());
478 statement
.executeUpdate();
482 private void storeProfile(
483 final Connection connection
, final RecipientId recipientId
, final Profile profile
484 ) throws SQLException
{
488 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 = ?
491 ).formatted(TABLE_RECIPIENT
);
492 try (final var statement
= connection
.prepareStatement(sql
)) {
493 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
494 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
495 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
496 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
497 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
498 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
499 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
500 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
501 statement
.setString(9,
504 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
505 statement
.setLong(10, recipientId
.id());
506 statement
.executeUpdate();
510 private void storeProfileKey(
511 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
512 ) throws SQLException
{
513 if (profileKey
!= null) {
514 final var recipientProfileKey
= getProfileKey(recipientId
);
515 if (profileKey
.equals(recipientProfileKey
)) {
516 final var recipientProfile
= getProfile(recipientId
);
517 if (recipientProfile
== null || (
518 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
519 && recipientProfile
.getUnidentifiedAccessMode()
520 != Profile
.UnidentifiedAccessMode
.DISABLED
530 SET profile_key = ?, profile_key_credential = NULL%s
533 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
534 try (final var statement
= connection
.prepareStatement(sql
)) {
535 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
536 statement
.setLong(2, recipientId
.id());
537 statement
.executeUpdate();
542 * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
543 * Has no effect, if the address contains only a number or a uuid.
545 private RecipientId
resolveRecipient(RecipientAddress address
, boolean isHighTrust
, boolean isSelf
) {
546 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
547 synchronized (recipientsLock
) {
548 try (final var connection
= database
.getConnection()) {
549 connection
.setAutoCommit(false);
550 pair
= resolveRecipientLocked(connection
, address
, isHighTrust
, isSelf
);
552 } catch (SQLException e
) {
553 throw new RuntimeException("Failed update recipient store", e
);
557 if (pair
.second().isPresent()) {
558 recipientMergeHandler
.mergeRecipients(pair
.first(), pair
.second().get());
559 try (final var connection
= database
.getConnection()) {
560 deleteRecipient(connection
, pair
.second().get());
561 } catch (SQLException e
) {
562 throw new RuntimeException("Failed update recipient store", e
);
568 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientLocked(
569 Connection connection
, RecipientAddress address
, boolean isHighTrust
, boolean isSelf
570 ) throws SQLException
{
571 if (isHighTrust
&& !isSelf
) {
572 if (selfAddressProvider
.getSelfAddress().matches(address
)) {
576 final var byNumber
= address
.number().isEmpty()
577 ? Optional
.<RecipientWithAddress
>empty()
578 : findByNumber(connection
, address
.number().get());
579 final var byUuid
= address
.uuid().isEmpty()
580 ? Optional
.<RecipientWithAddress
>empty()
581 : findByUuid(connection
, address
.uuid().get());
583 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
584 logger
.debug("Got new recipient, both uuid and number are unknown");
586 if (isHighTrust
|| address
.uuid().isEmpty() || address
.number().isEmpty()) {
587 return new Pair
<>(addNewRecipient(connection
, address
), Optional
.empty());
590 return new Pair
<>(addNewRecipient(connection
, new RecipientAddress(address
.uuid().get())),
594 if (!isHighTrust
|| address
.uuid().isEmpty() || address
.number().isEmpty() || byNumber
.equals(byUuid
)) {
595 return new Pair
<>(byUuid
.or(() -> byNumber
).map(RecipientWithAddress
::id
).get(), Optional
.empty());
598 if (byNumber
.isEmpty()) {
599 logger
.debug("Got recipient {} existing with uuid, updating with high trust number", byUuid
.get().id());
600 updateRecipientAddress(connection
, byUuid
.get().id(), address
);
601 return new Pair
<>(byUuid
.get().id(), Optional
.empty());
604 final var byNumberRecipient
= byNumber
.get();
606 if (byUuid
.isEmpty()) {
607 if (byNumberRecipient
.address().uuid().isPresent()) {
609 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
610 byNumberRecipient
.id());
612 updateRecipientAddress(connection
,
613 byNumberRecipient
.id(),
614 new RecipientAddress(byNumberRecipient
.address().uuid().get()));
615 return new Pair
<>(addNewRecipient(connection
, address
), Optional
.empty());
618 logger
.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
619 byNumberRecipient
.id());
620 updateRecipientAddress(connection
, byNumberRecipient
.id(), address
);
621 return new Pair
<>(byNumberRecipient
.id(), Optional
.empty());
624 final var byUuidRecipient
= byUuid
.get();
626 if (byNumberRecipient
.address().uuid().isPresent()) {
628 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
629 byNumberRecipient
.id(),
630 byUuidRecipient
.id());
632 updateRecipientAddress(connection
,
633 byNumberRecipient
.id(),
634 new RecipientAddress(byNumberRecipient
.address().uuid().get()));
635 updateRecipientAddress(connection
, byUuidRecipient
.id(), address
);
636 return new Pair
<>(byUuidRecipient
.id(), Optional
.empty());
639 logger
.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
640 byNumberRecipient
.id(),
641 byUuidRecipient
.id());
642 // Create a fixed RecipientId that won't update its id after merge
643 final var toBeMergedRecipientId
= new RecipientId(byNumberRecipient
.id().id(), null);
644 mergeRecipientsLocked(connection
, byUuidRecipient
.id(), toBeMergedRecipientId
);
645 removeRecipientAddress(connection
, toBeMergedRecipientId
);
646 updateRecipientAddress(connection
, byUuidRecipient
.id(), address
);
647 return new Pair
<>(byUuidRecipient
.id(), Optional
.of(toBeMergedRecipientId
));
650 private RecipientId
addNewRecipient(
651 final Connection connection
, final RecipientAddress address
652 ) throws SQLException
{
655 INSERT INTO %s (number, uuid)
658 ).formatted(TABLE_RECIPIENT
);
659 try (final var statement
= connection
.prepareStatement(sql
)) {
660 statement
.setString(1, address
.number().orElse(null));
661 statement
.setBytes(2, address
.uuid().map(UuidUtil
::toByteArray
).orElse(null));
662 statement
.executeUpdate();
663 final var generatedKeys
= statement
.getGeneratedKeys();
664 if (generatedKeys
.next()) {
665 final var recipientId
= new RecipientId(generatedKeys
.getLong(1), this);
666 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
669 throw new RuntimeException("Failed to add new recipient to database");
674 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
678 SET number = NULL, uuid = NULL
681 ).formatted(TABLE_RECIPIENT
);
682 try (final var statement
= connection
.prepareStatement(sql
)) {
683 statement
.setLong(1, recipientId
.id());
684 statement
.executeUpdate();
688 private void updateRecipientAddress(
689 Connection connection
, RecipientId recipientId
, final RecipientAddress address
690 ) throws SQLException
{
694 SET number = ?, uuid = ?
697 ).formatted(TABLE_RECIPIENT
);
698 try (final var statement
= connection
.prepareStatement(sql
)) {
699 statement
.setString(1, address
.number().orElse(null));
700 statement
.setBytes(2, address
.uuid().map(UuidUtil
::toByteArray
).orElse(null));
701 statement
.setLong(3, recipientId
.id());
702 statement
.executeUpdate();
706 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
712 ).formatted(TABLE_RECIPIENT
);
713 try (final var statement
= connection
.prepareStatement(sql
)) {
714 statement
.setLong(1, recipientId
.id());
715 statement
.executeUpdate();
719 private void mergeRecipientsLocked(
720 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
721 ) throws SQLException
{
722 final var contact
= getContact(connection
, recipientId
);
723 if (contact
== null) {
724 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
725 storeContact(connection
, recipientId
, toBeMergedContact
);
728 final var profileKey
= getProfileKey(connection
, recipientId
);
729 if (profileKey
== null) {
730 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
731 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
734 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
735 if (profileKeyCredential
== null) {
736 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
737 toBeMergedRecipientId
);
738 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
741 final var profile
= getProfile(connection
, recipientId
);
742 if (profile
== null) {
743 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
744 storeProfile(connection
, recipientId
, toBeMergedProfile
);
747 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
750 private Optional
<RecipientWithAddress
> findByNumber(
751 final Connection connection
, final String number
752 ) throws SQLException
{
754 SELECT r._id, r.number, r.uuid
757 """.formatted(TABLE_RECIPIENT
);
758 try (final var statement
= connection
.prepareStatement(sql
)) {
759 statement
.setString(1, number
);
760 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
764 private Optional
<RecipientWithAddress
> findByUuid(
765 final Connection connection
, final UUID uuid
766 ) throws SQLException
{
768 SELECT r._id, r.number, r.uuid
771 """.formatted(TABLE_RECIPIENT
);
772 try (final var statement
= connection
.prepareStatement(sql
)) {
773 statement
.setBytes(1, UuidUtil
.toByteArray(uuid
));
774 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
778 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
781 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
783 WHERE r._id = ? AND (%s)
785 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
786 try (final var statement
= connection
.prepareStatement(sql
)) {
787 statement
.setLong(1, recipientId
.id());
788 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
792 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
799 ).formatted(TABLE_RECIPIENT
);
800 try (final var statement
= connection
.prepareStatement(sql
)) {
801 statement
.setLong(1, recipientId
.id());
802 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
806 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
807 final Connection connection
, final RecipientId recipientId
808 ) throws SQLException
{
811 SELECT r.profile_key_credential
815 ).formatted(TABLE_RECIPIENT
);
816 try (final var statement
= connection
.prepareStatement(sql
)) {
817 statement
.setLong(1, recipientId
.id());
818 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
823 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
826 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
828 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
830 ).formatted(TABLE_RECIPIENT
);
831 try (final var statement
= connection
.prepareStatement(sql
)) {
832 statement
.setLong(1, recipientId
.id());
833 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
837 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
838 final var uuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
839 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
840 return new RecipientAddress(uuid
, number
);
843 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
844 return new RecipientId(resultSet
.getLong("_id"), this);
847 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
848 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
849 getRecipientAddressFromResultSet(resultSet
));
852 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
853 return new Recipient(getRecipientIdFromResultSet(resultSet
),
854 getRecipientAddressFromResultSet(resultSet
),
855 getContactFromResultSet(resultSet
),
856 getProfileKeyFromResultSet(resultSet
),
857 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
858 getProfileFromResultSet(resultSet
));
861 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
862 return new Contact(resultSet
.getString("given_name"),
863 resultSet
.getString("family_name"),
864 resultSet
.getString("color"),
865 resultSet
.getInt("expiration_time"),
866 resultSet
.getBoolean("blocked"),
867 resultSet
.getBoolean("archived"),
868 resultSet
.getBoolean("profile_sharing"));
871 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
872 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
873 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
874 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
875 resultSet
.getString("profile_given_name"),
876 resultSet
.getString("profile_family_name"),
877 resultSet
.getString("profile_about"),
878 resultSet
.getString("profile_about_emoji"),
879 resultSet
.getString("profile_avatar_url_path"),
880 resultSet
.getBytes("profile_mobile_coin_address"),
881 profileUnidentifiedAccessMode
== null
882 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
883 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
884 profileCapabilities
== null
886 : Arrays
.stream(profileCapabilities
.split(","))
887 .map(Profile
.Capability
::valueOfOrNull
)
888 .filter(Objects
::nonNull
)
889 .collect(Collectors
.toSet()));
892 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
893 final var profileKey
= resultSet
.getBytes("profile_key");
895 if (profileKey
== null) {
899 return new ProfileKey(profileKey
);
900 } catch (InvalidInputException ignored
) {
905 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
906 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
908 if (profileKeyCredential
== null) {
912 return new ExpiringProfileKeyCredential(profileKeyCredential
);
913 } catch (Throwable ignored
) {
918 public interface RecipientMergeHandler
{
920 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
);
923 private record RecipientWithAddress(RecipientId id
, RecipientAddress address
) {}