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
.PNI
;
16 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
17 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
18 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
20 import java
.sql
.Connection
;
21 import java
.sql
.ResultSet
;
22 import java
.sql
.SQLException
;
23 import java
.util
.ArrayList
;
24 import java
.util
.Arrays
;
25 import java
.util
.Collection
;
26 import java
.util
.HashMap
;
27 import java
.util
.List
;
29 import java
.util
.Objects
;
30 import java
.util
.Optional
;
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 public RecipientId
resolveRecipient(final String identifier
) {
150 if (UuidUtil
.isUuid(identifier
)) {
151 return resolveRecipient(ServiceId
.parseOrThrow(identifier
));
153 return resolveRecipientByNumber(identifier
);
157 private RecipientId
resolveRecipientByNumber(final String number
) {
158 synchronized (recipientsLock
) {
159 final RecipientId recipientId
;
160 try (final var connection
= database
.getConnection()) {
161 connection
.setAutoCommit(false);
162 recipientId
= resolveRecipientLocked(connection
, number
);
164 } catch (SQLException e
) {
165 throw new RuntimeException("Failed read recipient store", e
);
172 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
173 synchronized (recipientsLock
) {
174 final RecipientId recipientId
;
175 try (final var connection
= database
.getConnection()) {
176 connection
.setAutoCommit(false);
177 recipientId
= resolveRecipientLocked(connection
, serviceId
);
179 } catch (SQLException e
) {
180 throw new RuntimeException("Failed read recipient store", e
);
187 * Should only be used for recipientIds from the database.
188 * Where the foreign key relations ensure a valid recipientId.
191 public RecipientId
create(final long recipientId
) {
192 return new RecipientId(recipientId
, this);
195 public RecipientId
resolveRecipient(
196 final String number
, Supplier
<ServiceId
> serviceIdSupplier
197 ) throws UnregisteredRecipientException
{
198 final Optional
<RecipientWithAddress
> byNumber
;
199 try (final var connection
= database
.getConnection()) {
200 byNumber
= findByNumber(connection
, number
);
201 } catch (SQLException e
) {
202 throw new RuntimeException("Failed read from recipient store", e
);
204 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
205 final var serviceId
= serviceIdSupplier
.get();
206 if (serviceId
== null) {
207 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
211 return resolveRecipient(serviceId
);
213 return byNumber
.get().id();
216 public RecipientId
resolveRecipient(RecipientAddress address
) {
217 synchronized (recipientsLock
) {
218 final RecipientId recipientId
;
219 try (final var connection
= database
.getConnection()) {
220 connection
.setAutoCommit(false);
221 recipientId
= resolveRecipientLocked(connection
, address
);
223 } catch (SQLException e
) {
224 throw new RuntimeException("Failed read recipient store", e
);
231 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
232 return resolveRecipientTrusted(address
, true);
235 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
236 return resolveRecipientTrusted(address
, false);
240 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
241 return resolveRecipientTrusted(new RecipientAddress(address
), false);
245 public RecipientId
resolveRecipientTrusted(
246 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
248 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
249 return resolveRecipientTrusted(new RecipientAddress(serviceId
, number
), false);
253 public void storeContact(RecipientId recipientId
, final Contact contact
) {
254 try (final var connection
= database
.getConnection()) {
255 storeContact(connection
, recipientId
, contact
);
256 } catch (SQLException e
) {
257 throw new RuntimeException("Failed update recipient store", e
);
262 public Contact
getContact(RecipientId recipientId
) {
263 try (final var connection
= database
.getConnection()) {
264 return getContact(connection
, recipientId
);
265 } catch (SQLException e
) {
266 throw new RuntimeException("Failed read from recipient store", e
);
271 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
274 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
276 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
278 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
279 try (final var connection
= database
.getConnection()) {
280 try (final var statement
= connection
.prepareStatement(sql
)) {
281 try (var result
= Utils
.executeQueryForStream(statement
,
282 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
283 getContactFromResultSet(resultSet
)))) {
284 return result
.toList();
287 } catch (SQLException e
) {
288 throw new RuntimeException("Failed read from recipient store", e
);
292 public List
<Recipient
> getRecipients(
293 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
295 final var sqlWhere
= new ArrayList
<String
>();
297 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
299 if (blocked
.isPresent()) {
300 sqlWhere
.add("r.blocked = ?");
302 if (!recipientIds
.isEmpty()) {
303 final var recipientIdsCommaSeparated
= recipientIds
.stream()
304 .map(recipientId
-> String
.valueOf(recipientId
.id()))
305 .collect(Collectors
.joining(","));
306 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
312 r.profile_key, r.profile_key_credential,
313 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
314 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
316 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
318 ).formatted(TABLE_RECIPIENT
, sqlWhere
.size() == 0 ?
"TRUE" : String
.join(" AND ", sqlWhere
));
319 try (final var connection
= database
.getConnection()) {
320 try (final var statement
= connection
.prepareStatement(sql
)) {
321 if (blocked
.isPresent()) {
322 statement
.setBoolean(1, blocked
.get());
324 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
325 return result
.filter(r
-> name
.isEmpty() || (
326 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
327 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).toList();
330 } catch (SQLException e
) {
331 throw new RuntimeException("Failed read from recipient store", e
);
335 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
338 SELECT r.uuid, r.profile_key
340 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
342 ).formatted(TABLE_RECIPIENT
);
343 try (final var connection
= database
.getConnection()) {
344 try (final var statement
= connection
.prepareStatement(sql
)) {
345 return Utils
.executeQueryForStream(statement
, resultSet
-> {
346 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
347 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
348 return new Pair
<>(serviceId
, profileKey
);
349 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
351 } catch (SQLException e
) {
352 throw new RuntimeException("Failed read from recipient store", e
);
357 public void deleteContact(RecipientId recipientId
) {
358 storeContact(recipientId
, null);
361 public void deleteRecipientData(RecipientId recipientId
) {
362 logger
.debug("Deleting recipient data for {}", recipientId
);
363 try (final var connection
= database
.getConnection()) {
364 connection
.setAutoCommit(false);
365 storeContact(connection
, recipientId
, null);
366 storeProfile(connection
, recipientId
, null);
367 storeProfileKey(connection
, recipientId
, null, false);
368 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
369 deleteRecipient(connection
, recipientId
);
371 } catch (SQLException e
) {
372 throw new RuntimeException("Failed update recipient store", e
);
377 public Profile
getProfile(final RecipientId recipientId
) {
378 try (final var connection
= database
.getConnection()) {
379 return getProfile(connection
, recipientId
);
380 } catch (SQLException e
) {
381 throw new RuntimeException("Failed read from recipient store", e
);
386 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
387 try (final var connection
= database
.getConnection()) {
388 return getProfileKey(connection
, recipientId
);
389 } catch (SQLException e
) {
390 throw new RuntimeException("Failed read from recipient store", e
);
395 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
396 try (final var connection
= database
.getConnection()) {
397 return getExpiringProfileKeyCredential(connection
, recipientId
);
398 } catch (SQLException e
) {
399 throw new RuntimeException("Failed read from recipient store", e
);
404 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
405 try (final var connection
= database
.getConnection()) {
406 storeProfile(connection
, recipientId
, profile
);
407 } catch (SQLException e
) {
408 throw new RuntimeException("Failed update recipient store", e
);
413 public void storeSelfProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
414 try (final var connection
= database
.getConnection()) {
415 storeProfileKey(connection
, recipientId
, profileKey
, false);
416 } catch (SQLException e
) {
417 throw new RuntimeException("Failed update recipient store", e
);
422 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
423 try (final var connection
= database
.getConnection()) {
424 storeProfileKey(connection
, recipientId
, profileKey
, true);
425 } catch (SQLException e
) {
426 throw new RuntimeException("Failed update recipient store", e
);
431 public void storeExpiringProfileKeyCredential(
432 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
434 try (final var connection
= database
.getConnection()) {
435 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
436 } catch (SQLException e
) {
437 throw new RuntimeException("Failed update recipient store", e
);
441 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
442 logger
.debug("Migrating legacy recipients to database");
443 long start
= System
.nanoTime();
446 INSERT INTO %s (_id, number, uuid)
449 ).formatted(TABLE_RECIPIENT
);
450 try (final var connection
= database
.getConnection()) {
451 connection
.setAutoCommit(false);
452 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
453 statement
.executeUpdate();
455 try (final var statement
= connection
.prepareStatement(sql
)) {
456 for (final var recipient
: recipients
.values()) {
457 statement
.setLong(1, recipient
.getRecipientId().id());
458 statement
.setString(2, recipient
.getAddress().number().orElse(null));
459 statement
.setBytes(3,
460 recipient
.getAddress()
462 .map(ServiceId
::uuid
)
463 .map(UuidUtil
::toByteArray
)
465 statement
.executeUpdate();
468 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
470 for (final var recipient
: recipients
.values()) {
471 if (recipient
.getContact() != null) {
472 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
474 if (recipient
.getProfile() != null) {
475 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
477 if (recipient
.getProfileKey() != null) {
478 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
480 if (recipient
.getExpiringProfileKeyCredential() != null) {
481 storeExpiringProfileKeyCredential(connection
,
482 recipient
.getRecipientId(),
483 recipient
.getExpiringProfileKeyCredential());
487 } catch (SQLException e
) {
488 throw new RuntimeException("Failed update recipient store", e
);
490 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
493 long getActualRecipientId(long recipientId
) {
494 while (recipientsMerged
.containsKey(recipientId
)) {
495 final var newRecipientId
= recipientsMerged
.get(recipientId
);
496 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
497 recipientId
= newRecipientId
;
502 private void storeContact(
503 final Connection connection
, final RecipientId recipientId
, final Contact contact
504 ) throws SQLException
{
508 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
511 ).formatted(TABLE_RECIPIENT
);
512 try (final var statement
= connection
.prepareStatement(sql
)) {
513 statement
.setString(1, contact
== null ?
null : contact
.getGivenName());
514 statement
.setString(2, contact
== null ?
null : contact
.getFamilyName());
515 statement
.setInt(3, contact
== null ?
0 : contact
.getMessageExpirationTime());
516 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
517 statement
.setString(5, contact
== null ?
null : contact
.getColor());
518 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
519 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
520 statement
.setLong(8, recipientId
.id());
521 statement
.executeUpdate();
525 private void storeExpiringProfileKeyCredential(
526 final Connection connection
,
527 final RecipientId recipientId
,
528 final ExpiringProfileKeyCredential profileKeyCredential
529 ) throws SQLException
{
533 SET profile_key_credential = ?
536 ).formatted(TABLE_RECIPIENT
);
537 try (final var statement
= connection
.prepareStatement(sql
)) {
538 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
539 statement
.setLong(2, recipientId
.id());
540 statement
.executeUpdate();
544 private void storeProfile(
545 final Connection connection
, final RecipientId recipientId
, final Profile profile
546 ) throws SQLException
{
550 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 = ?
553 ).formatted(TABLE_RECIPIENT
);
554 try (final var statement
= connection
.prepareStatement(sql
)) {
555 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
556 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
557 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
558 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
559 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
560 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
561 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
562 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
563 statement
.setString(9,
566 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
567 statement
.setLong(10, recipientId
.id());
568 statement
.executeUpdate();
572 private void storeProfileKey(
573 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
574 ) throws SQLException
{
575 if (profileKey
!= null) {
576 final var recipientProfileKey
= getProfileKey(recipientId
);
577 if (profileKey
.equals(recipientProfileKey
)) {
578 final var recipientProfile
= getProfile(recipientId
);
579 if (recipientProfile
== null || (
580 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
581 && recipientProfile
.getUnidentifiedAccessMode()
582 != Profile
.UnidentifiedAccessMode
.DISABLED
592 SET profile_key = ?, profile_key_credential = NULL%s
595 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
596 try (final var statement
= connection
.prepareStatement(sql
)) {
597 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
598 statement
.setLong(2, recipientId
.id());
599 statement
.executeUpdate();
603 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
604 final Pair
<RecipientId
, Optional
<RecipientId
>> pair
;
605 synchronized (recipientsLock
) {
606 try (final var connection
= database
.getConnection()) {
607 connection
.setAutoCommit(false);
608 pair
= resolveRecipientTrustedLocked(connection
, address
, isSelf
);
610 } catch (SQLException e
) {
611 throw new RuntimeException("Failed update recipient store", e
);
615 if (pair
.second().isPresent()) {
616 try (final var connection
= database
.getConnection()) {
617 recipientMergeHandler
.mergeRecipients(connection
, pair
.first(), pair
.second().get());
618 deleteRecipient(connection
, pair
.second().get());
619 } catch (SQLException e
) {
620 throw new RuntimeException("Failed update recipient store", e
);
626 private Pair
<RecipientId
, Optional
<RecipientId
>> resolveRecipientTrustedLocked(
627 Connection connection
, RecipientAddress address
, boolean isSelf
628 ) throws SQLException
{
630 if (selfAddressProvider
.getSelfAddress().matches(address
)) {
631 return new Pair
<>(resolveRecipientLocked(connection
, address
), Optional
.empty());
634 final var byNumber
= address
.number().isEmpty()
635 ? Optional
.<RecipientWithAddress
>empty()
636 : findByNumber(connection
, address
.number().get());
637 final var byUuid
= address
.serviceId().isEmpty()
638 ? Optional
.<RecipientWithAddress
>empty()
639 : findByServiceId(connection
, address
.serviceId().get());
641 if (byNumber
.isEmpty() && byUuid
.isEmpty()) {
642 logger
.debug("Got new recipient, both uuid and number are unknown");
643 return new Pair
<>(addNewRecipient(connection
, address
), Optional
.empty());
646 if (address
.serviceId().isEmpty() || address
.number().isEmpty() || byNumber
.equals(byUuid
)) {
647 return new Pair
<>(byUuid
.or(() -> byNumber
).map(RecipientWithAddress
::id
).get(), Optional
.empty());
650 if (byNumber
.isEmpty()) {
651 logger
.debug("Got recipient {} existing with uuid, updating with high trust number", byUuid
.get().id());
652 updateRecipientAddress(connection
, byUuid
.get().id(), address
);
653 return new Pair
<>(byUuid
.get().id(), Optional
.empty());
656 final var byNumberRecipient
= byNumber
.get();
658 if (byUuid
.isEmpty()) {
659 if (byNumberRecipient
.address().serviceId().isPresent()) {
661 "Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
662 byNumberRecipient
.id());
664 updateRecipientAddress(connection
,
665 byNumberRecipient
.id(),
666 new RecipientAddress(byNumberRecipient
.address().serviceId().get()));
667 return new Pair
<>(addNewRecipient(connection
, address
), Optional
.empty());
670 logger
.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
671 byNumberRecipient
.id());
672 updateRecipientAddress(connection
, byNumberRecipient
.id(), address
);
673 return new Pair
<>(byNumberRecipient
.id(), Optional
.empty());
676 final var byUuidRecipient
= byUuid
.get();
678 if (byNumberRecipient
.address().serviceId().isPresent()) {
680 "Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
681 byNumberRecipient
.id(),
682 byUuidRecipient
.id());
684 updateRecipientAddress(connection
,
685 byNumberRecipient
.id(),
686 new RecipientAddress(byNumberRecipient
.address().serviceId().get()));
687 updateRecipientAddress(connection
, byUuidRecipient
.id(), address
);
688 return new Pair
<>(byUuidRecipient
.id(), Optional
.empty());
691 logger
.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
692 byNumberRecipient
.id(),
693 byUuidRecipient
.id());
694 // Create a fixed RecipientId that won't update its id after merge
695 final var toBeMergedRecipientId
= new RecipientId(byNumberRecipient
.id().id(), null);
696 mergeRecipientsLocked(connection
, byUuidRecipient
.id(), toBeMergedRecipientId
);
697 removeRecipientAddress(connection
, toBeMergedRecipientId
);
698 updateRecipientAddress(connection
, byUuidRecipient
.id(), address
);
699 return new Pair
<>(byUuidRecipient
.id(), Optional
.of(toBeMergedRecipientId
));
702 private RecipientId
resolveRecipientLocked(
703 Connection connection
, RecipientAddress address
704 ) throws SQLException
{
705 final var byServiceId
= address
.serviceId().isEmpty()
706 ? Optional
.<RecipientWithAddress
>empty()
707 : findByServiceId(connection
, address
.serviceId().get());
709 if (byServiceId
.isPresent()) {
710 return byServiceId
.get().id();
713 final var byPni
= address
.pni().isEmpty()
714 ? Optional
.<RecipientWithAddress
>empty()
715 : findByServiceId(connection
, address
.pni().get());
717 if (byPni
.isPresent()) {
718 return byPni
.get().id();
721 final var byNumber
= address
.number().isEmpty()
722 ? Optional
.<RecipientWithAddress
>empty()
723 : findByNumber(connection
, address
.number().get());
725 if (byNumber
.isPresent()) {
726 return byNumber
.get().id();
729 logger
.debug("Got new recipient, both serviceId and number are unknown");
731 if (address
.serviceId().isEmpty()) {
732 return addNewRecipient(connection
, address
);
735 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
738 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
739 final var recipient
= findByServiceId(connection
, serviceId
);
741 if (recipient
.isEmpty()) {
742 logger
.debug("Got new recipient, serviceId is unknown");
743 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
746 return recipient
.get().id();
749 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
750 final var recipient
= findByNumber(connection
, number
);
752 if (recipient
.isEmpty()) {
753 logger
.debug("Got new recipient, number is unknown");
754 return addNewRecipient(connection
, new RecipientAddress(null, number
));
757 return recipient
.get().id();
760 private RecipientId
addNewRecipient(
761 final Connection connection
, final RecipientAddress address
762 ) throws SQLException
{
765 INSERT INTO %s (number, uuid)
768 ).formatted(TABLE_RECIPIENT
);
769 try (final var statement
= connection
.prepareStatement(sql
)) {
770 statement
.setString(1, address
.number().orElse(null));
771 statement
.setBytes(2, address
.serviceId().map(ServiceId
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
772 statement
.executeUpdate();
773 final var generatedKeys
= statement
.getGeneratedKeys();
774 if (generatedKeys
.next()) {
775 final var recipientId
= new RecipientId(generatedKeys
.getLong(1), this);
776 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
779 throw new RuntimeException("Failed to add new recipient to database");
784 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
788 SET number = NULL, uuid = NULL
791 ).formatted(TABLE_RECIPIENT
);
792 try (final var statement
= connection
.prepareStatement(sql
)) {
793 statement
.setLong(1, recipientId
.id());
794 statement
.executeUpdate();
798 private void updateRecipientAddress(
799 Connection connection
, RecipientId recipientId
, final RecipientAddress address
800 ) throws SQLException
{
804 SET number = ?, uuid = ?
807 ).formatted(TABLE_RECIPIENT
);
808 try (final var statement
= connection
.prepareStatement(sql
)) {
809 statement
.setString(1, address
.number().orElse(null));
810 statement
.setBytes(2, address
.serviceId().map(ServiceId
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
811 statement
.setLong(3, recipientId
.id());
812 statement
.executeUpdate();
816 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
822 ).formatted(TABLE_RECIPIENT
);
823 try (final var statement
= connection
.prepareStatement(sql
)) {
824 statement
.setLong(1, recipientId
.id());
825 statement
.executeUpdate();
829 private void mergeRecipientsLocked(
830 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
831 ) throws SQLException
{
832 final var contact
= getContact(connection
, recipientId
);
833 if (contact
== null) {
834 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
835 storeContact(connection
, recipientId
, toBeMergedContact
);
838 final var profileKey
= getProfileKey(connection
, recipientId
);
839 if (profileKey
== null) {
840 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
841 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
844 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
845 if (profileKeyCredential
== null) {
846 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
847 toBeMergedRecipientId
);
848 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
851 final var profile
= getProfile(connection
, recipientId
);
852 if (profile
== null) {
853 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
854 storeProfile(connection
, recipientId
, toBeMergedProfile
);
857 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
860 private Optional
<RecipientWithAddress
> findByNumber(
861 final Connection connection
, final String number
862 ) throws SQLException
{
864 SELECT r._id, r.number, r.uuid
867 """.formatted(TABLE_RECIPIENT
);
868 try (final var statement
= connection
.prepareStatement(sql
)) {
869 statement
.setString(1, number
);
870 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
874 private Optional
<RecipientWithAddress
> findByServiceId(
875 final Connection connection
, final ServiceId serviceId
876 ) throws SQLException
{
878 SELECT r._id, r.number, r.uuid
881 """.formatted(TABLE_RECIPIENT
);
882 try (final var statement
= connection
.prepareStatement(sql
)) {
883 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.uuid()));
884 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
888 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
891 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
893 WHERE r._id = ? AND (%s)
895 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
896 try (final var statement
= connection
.prepareStatement(sql
)) {
897 statement
.setLong(1, recipientId
.id());
898 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
902 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
909 ).formatted(TABLE_RECIPIENT
);
910 try (final var statement
= connection
.prepareStatement(sql
)) {
911 statement
.setLong(1, recipientId
.id());
912 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
916 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
917 final Connection connection
, final RecipientId recipientId
918 ) throws SQLException
{
921 SELECT r.profile_key_credential
925 ).formatted(TABLE_RECIPIENT
);
926 try (final var statement
= connection
.prepareStatement(sql
)) {
927 statement
.setLong(1, recipientId
.id());
928 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
933 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
936 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
938 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
940 ).formatted(TABLE_RECIPIENT
);
941 try (final var statement
= connection
.prepareStatement(sql
)) {
942 statement
.setLong(1, recipientId
.id());
943 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
947 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
948 final var serviceId
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(ServiceId
::parseOrNull
);
949 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
950 return new RecipientAddress(serviceId
, Optional
.empty(), number
);
953 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
954 return new RecipientId(resultSet
.getLong("_id"), this);
957 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
958 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
959 getRecipientAddressFromResultSet(resultSet
));
962 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
963 return new Recipient(getRecipientIdFromResultSet(resultSet
),
964 getRecipientAddressFromResultSet(resultSet
),
965 getContactFromResultSet(resultSet
),
966 getProfileKeyFromResultSet(resultSet
),
967 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
968 getProfileFromResultSet(resultSet
));
971 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
972 return new Contact(resultSet
.getString("given_name"),
973 resultSet
.getString("family_name"),
974 resultSet
.getString("color"),
975 resultSet
.getInt("expiration_time"),
976 resultSet
.getBoolean("blocked"),
977 resultSet
.getBoolean("archived"),
978 resultSet
.getBoolean("profile_sharing"));
981 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
982 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
983 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
984 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
985 resultSet
.getString("profile_given_name"),
986 resultSet
.getString("profile_family_name"),
987 resultSet
.getString("profile_about"),
988 resultSet
.getString("profile_about_emoji"),
989 resultSet
.getString("profile_avatar_url_path"),
990 resultSet
.getBytes("profile_mobile_coin_address"),
991 profileUnidentifiedAccessMode
== null
992 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
993 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
994 profileCapabilities
== null
996 : Arrays
.stream(profileCapabilities
.split(","))
997 .map(Profile
.Capability
::valueOfOrNull
)
998 .filter(Objects
::nonNull
)
999 .collect(Collectors
.toSet()));
1002 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1003 final var profileKey
= resultSet
.getBytes("profile_key");
1005 if (profileKey
== null) {
1009 return new ProfileKey(profileKey
);
1010 } catch (InvalidInputException ignored
) {
1015 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1016 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1018 if (profileKeyCredential
== null) {
1022 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1023 } catch (Throwable ignored
) {
1028 public interface RecipientMergeHandler
{
1030 void mergeRecipients(
1031 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1032 ) throws SQLException
;
1035 private record RecipientWithAddress(RecipientId id
, RecipientAddress address
) {}