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,
59 profile_key_credential BLOB,
65 expiration_time INTEGER NOT NULL DEFAULT 0,
66 blocked INTEGER NOT NULL DEFAULT FALSE,
67 archived INTEGER NOT NULL DEFAULT FALSE,
68 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
70 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
71 profile_given_name TEXT,
72 profile_family_name TEXT,
74 profile_about_emoji TEXT,
75 profile_avatar_url_path TEXT,
76 profile_mobile_coin_address BLOB,
77 profile_unidentified_access_mode TEXT,
78 profile_capabilities TEXT
84 public RecipientStore(
85 final RecipientMergeHandler recipientMergeHandler
,
86 final SelfAddressProvider selfAddressProvider
,
87 final Database database
89 this.recipientMergeHandler
= recipientMergeHandler
;
90 this.selfAddressProvider
= selfAddressProvider
;
91 this.database
= database
;
94 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
97 SELECT r.number, r.uuid, r.pni, r.username
101 ).formatted(TABLE_RECIPIENT
);
102 try (final var connection
= database
.getConnection()) {
103 try (final var statement
= connection
.prepareStatement(sql
)) {
104 statement
.setLong(1, recipientId
.id());
105 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
107 } catch (SQLException e
) {
108 throw new RuntimeException("Failed read from recipient store", e
);
112 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
117 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
119 ).formatted(TABLE_RECIPIENT
);
120 try (final var connection
= database
.getConnection()) {
121 try (final var statement
= connection
.prepareStatement(sql
)) {
122 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
123 return result
.toList();
126 } catch (SQLException e
) {
127 throw new RuntimeException("Failed read from recipient store", e
);
132 public RecipientId
resolveRecipient(final long rawRecipientId
) {
139 ).formatted(TABLE_RECIPIENT
);
140 try (final var connection
= database
.getConnection()) {
141 try (final var statement
= connection
.prepareStatement(sql
)) {
142 statement
.setLong(1, rawRecipientId
);
143 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
145 } catch (SQLException e
) {
146 throw new RuntimeException("Failed read from recipient store", e
);
151 public RecipientId
resolveRecipient(final String identifier
) {
152 if (UuidUtil
.isUuid(identifier
)) {
153 return resolveRecipient(ServiceId
.parseOrThrow(identifier
));
155 return resolveRecipientByNumber(identifier
);
159 private RecipientId
resolveRecipientByNumber(final String number
) {
160 synchronized (recipientsLock
) {
161 final RecipientId recipientId
;
162 try (final var connection
= database
.getConnection()) {
163 connection
.setAutoCommit(false);
164 recipientId
= resolveRecipientLocked(connection
, number
);
166 } catch (SQLException e
) {
167 throw new RuntimeException("Failed read recipient store", e
);
174 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
175 synchronized (recipientsLock
) {
176 final RecipientId recipientId
;
177 try (final var connection
= database
.getConnection()) {
178 connection
.setAutoCommit(false);
179 recipientId
= resolveRecipientLocked(connection
, serviceId
);
181 } catch (SQLException e
) {
182 throw new RuntimeException("Failed read recipient store", e
);
189 * Should only be used for recipientIds from the database.
190 * Where the foreign key relations ensure a valid recipientId.
193 public RecipientId
create(final long recipientId
) {
194 return new RecipientId(recipientId
, this);
197 public RecipientId
resolveRecipientByNumber(
198 final String number
, Supplier
<ServiceId
> serviceIdSupplier
199 ) throws UnregisteredRecipientException
{
200 final Optional
<RecipientWithAddress
> byNumber
;
201 try (final var connection
= database
.getConnection()) {
202 byNumber
= findByNumber(connection
, number
);
203 } catch (SQLException e
) {
204 throw new RuntimeException("Failed read from recipient store", e
);
206 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
207 final var serviceId
= serviceIdSupplier
.get();
208 if (serviceId
== null) {
209 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
213 return resolveRecipient(serviceId
);
215 return byNumber
.get().id();
218 public RecipientId
resolveRecipientByUsername(
219 final String username
, Supplier
<ServiceId
> serviceIdSupplier
220 ) throws UnregisteredRecipientException
{
221 final Optional
<RecipientWithAddress
> byUsername
;
222 try (final var connection
= database
.getConnection()) {
223 byUsername
= findByUsername(connection
, username
);
224 } catch (SQLException e
) {
225 throw new RuntimeException("Failed read from recipient store", e
);
227 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
228 final var serviceId
= serviceIdSupplier
.get();
229 if (serviceId
== null) {
230 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
235 return resolveRecipient(serviceId
);
237 return byUsername
.get().id();
240 public RecipientId
resolveRecipient(RecipientAddress address
) {
241 synchronized (recipientsLock
) {
242 final RecipientId recipientId
;
243 try (final var connection
= database
.getConnection()) {
244 connection
.setAutoCommit(false);
245 recipientId
= resolveRecipientLocked(connection
, address
);
247 } catch (SQLException e
) {
248 throw new RuntimeException("Failed read recipient store", e
);
255 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
256 return resolveRecipientTrusted(address
, true);
259 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
260 return resolveRecipientTrusted(address
, false);
264 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
265 return resolveRecipientTrusted(new RecipientAddress(address
), false);
269 public RecipientId
resolveRecipientTrusted(
270 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
272 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
273 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()), false);
277 public RecipientId
resolveRecipientTrusted(final ServiceId serviceId
, final String username
) {
278 return resolveRecipientTrusted(new RecipientAddress(serviceId
, null, null, username
), false);
281 public RecipientId
resolveRecipientTrusted(
282 final ACI aci
, final String username
284 return resolveRecipientTrusted(new RecipientAddress(Optional
.of(aci
),
287 Optional
.of(username
)), false);
291 public void storeContact(RecipientId recipientId
, final Contact contact
) {
292 try (final var connection
= database
.getConnection()) {
293 storeContact(connection
, recipientId
, contact
);
294 } catch (SQLException e
) {
295 throw new RuntimeException("Failed update recipient store", e
);
300 public Contact
getContact(RecipientId recipientId
) {
301 try (final var connection
= database
.getConnection()) {
302 return getContact(connection
, recipientId
);
303 } catch (SQLException e
) {
304 throw new RuntimeException("Failed read from recipient store", e
);
309 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
312 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
314 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
316 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
317 try (final var connection
= database
.getConnection()) {
318 try (final var statement
= connection
.prepareStatement(sql
)) {
319 try (var result
= Utils
.executeQueryForStream(statement
,
320 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
321 getContactFromResultSet(resultSet
)))) {
322 return result
.toList();
325 } catch (SQLException e
) {
326 throw new RuntimeException("Failed read from recipient store", e
);
330 public List
<Recipient
> getRecipients(
331 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
333 final var sqlWhere
= new ArrayList
<String
>();
335 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
337 if (blocked
.isPresent()) {
338 sqlWhere
.add("r.blocked = ?");
340 if (!recipientIds
.isEmpty()) {
341 final var recipientIdsCommaSeparated
= recipientIds
.stream()
342 .map(recipientId
-> String
.valueOf(recipientId
.id()))
343 .collect(Collectors
.joining(","));
344 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
349 r.number, r.uuid, r.pni, r.username,
350 r.profile_key, r.profile_key_credential,
351 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
352 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
354 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
356 ).formatted(TABLE_RECIPIENT
, sqlWhere
.size() == 0 ?
"TRUE" : String
.join(" AND ", sqlWhere
));
357 try (final var connection
= database
.getConnection()) {
358 try (final var statement
= connection
.prepareStatement(sql
)) {
359 if (blocked
.isPresent()) {
360 statement
.setBoolean(1, blocked
.get());
362 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
363 return result
.filter(r
-> name
.isEmpty() || (
364 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
365 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).toList();
368 } catch (SQLException e
) {
369 throw new RuntimeException("Failed read from recipient store", e
);
373 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
376 SELECT r.uuid, r.profile_key
378 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
380 ).formatted(TABLE_RECIPIENT
);
381 try (final var connection
= database
.getConnection()) {
382 try (final var statement
= connection
.prepareStatement(sql
)) {
383 return Utils
.executeQueryForStream(statement
, resultSet
-> {
384 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
385 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
386 return new Pair
<>(serviceId
, profileKey
);
387 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
389 } catch (SQLException e
) {
390 throw new RuntimeException("Failed read from recipient store", e
);
395 public void deleteContact(RecipientId recipientId
) {
396 storeContact(recipientId
, null);
399 public void deleteRecipientData(RecipientId recipientId
) {
400 logger
.debug("Deleting recipient data for {}", recipientId
);
401 try (final var connection
= database
.getConnection()) {
402 connection
.setAutoCommit(false);
403 storeContact(connection
, recipientId
, null);
404 storeProfile(connection
, recipientId
, null);
405 storeProfileKey(connection
, recipientId
, null, false);
406 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
407 deleteRecipient(connection
, recipientId
);
409 } catch (SQLException e
) {
410 throw new RuntimeException("Failed update recipient store", e
);
415 public Profile
getProfile(final RecipientId recipientId
) {
416 try (final var connection
= database
.getConnection()) {
417 return getProfile(connection
, recipientId
);
418 } catch (SQLException e
) {
419 throw new RuntimeException("Failed read from recipient store", e
);
424 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
425 try (final var connection
= database
.getConnection()) {
426 return getProfileKey(connection
, recipientId
);
427 } catch (SQLException e
) {
428 throw new RuntimeException("Failed read from recipient store", e
);
433 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
434 try (final var connection
= database
.getConnection()) {
435 return getExpiringProfileKeyCredential(connection
, recipientId
);
436 } catch (SQLException e
) {
437 throw new RuntimeException("Failed read from recipient store", e
);
442 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
443 try (final var connection
= database
.getConnection()) {
444 storeProfile(connection
, recipientId
, profile
);
445 } catch (SQLException e
) {
446 throw new RuntimeException("Failed update recipient store", e
);
451 public void storeSelfProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
452 try (final var connection
= database
.getConnection()) {
453 storeProfileKey(connection
, recipientId
, profileKey
, false);
454 } catch (SQLException e
) {
455 throw new RuntimeException("Failed update recipient store", e
);
460 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
461 try (final var connection
= database
.getConnection()) {
462 storeProfileKey(connection
, recipientId
, profileKey
, true);
463 } catch (SQLException e
) {
464 throw new RuntimeException("Failed update recipient store", e
);
469 public void storeExpiringProfileKeyCredential(
470 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
472 try (final var connection
= database
.getConnection()) {
473 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
474 } catch (SQLException e
) {
475 throw new RuntimeException("Failed update recipient store", e
);
479 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
480 logger
.debug("Migrating legacy recipients to database");
481 long start
= System
.nanoTime();
484 INSERT INTO %s (_id, number, uuid)
487 ).formatted(TABLE_RECIPIENT
);
488 try (final var connection
= database
.getConnection()) {
489 connection
.setAutoCommit(false);
490 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
491 statement
.executeUpdate();
493 try (final var statement
= connection
.prepareStatement(sql
)) {
494 for (final var recipient
: recipients
.values()) {
495 statement
.setLong(1, recipient
.getRecipientId().id());
496 statement
.setString(2, recipient
.getAddress().number().orElse(null));
497 statement
.setBytes(3,
498 recipient
.getAddress()
500 .map(ServiceId
::uuid
)
501 .map(UuidUtil
::toByteArray
)
503 statement
.executeUpdate();
506 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
508 for (final var recipient
: recipients
.values()) {
509 if (recipient
.getContact() != null) {
510 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
512 if (recipient
.getProfile() != null) {
513 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
515 if (recipient
.getProfileKey() != null) {
516 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
518 if (recipient
.getExpiringProfileKeyCredential() != null) {
519 storeExpiringProfileKeyCredential(connection
,
520 recipient
.getRecipientId(),
521 recipient
.getExpiringProfileKeyCredential());
525 } catch (SQLException e
) {
526 throw new RuntimeException("Failed update recipient store", e
);
528 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
531 long getActualRecipientId(long recipientId
) {
532 while (recipientsMerged
.containsKey(recipientId
)) {
533 final var newRecipientId
= recipientsMerged
.get(recipientId
);
534 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
535 recipientId
= newRecipientId
;
540 private void storeContact(
541 final Connection connection
, final RecipientId recipientId
, final Contact contact
542 ) throws SQLException
{
546 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
549 ).formatted(TABLE_RECIPIENT
);
550 try (final var statement
= connection
.prepareStatement(sql
)) {
551 statement
.setString(1, contact
== null ?
null : contact
.getGivenName());
552 statement
.setString(2, contact
== null ?
null : contact
.getFamilyName());
553 statement
.setInt(3, contact
== null ?
0 : contact
.getMessageExpirationTime());
554 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
555 statement
.setString(5, contact
== null ?
null : contact
.getColor());
556 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
557 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
558 statement
.setLong(8, recipientId
.id());
559 statement
.executeUpdate();
563 private void storeExpiringProfileKeyCredential(
564 final Connection connection
,
565 final RecipientId recipientId
,
566 final ExpiringProfileKeyCredential profileKeyCredential
567 ) throws SQLException
{
571 SET profile_key_credential = ?
574 ).formatted(TABLE_RECIPIENT
);
575 try (final var statement
= connection
.prepareStatement(sql
)) {
576 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
577 statement
.setLong(2, recipientId
.id());
578 statement
.executeUpdate();
582 private void storeProfile(
583 final Connection connection
, final RecipientId recipientId
, final Profile profile
584 ) throws SQLException
{
588 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 = ?
591 ).formatted(TABLE_RECIPIENT
);
592 try (final var statement
= connection
.prepareStatement(sql
)) {
593 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
594 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
595 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
596 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
597 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
598 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
599 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
600 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
601 statement
.setString(9,
604 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
605 statement
.setLong(10, recipientId
.id());
606 statement
.executeUpdate();
610 private void storeProfileKey(
611 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
612 ) throws SQLException
{
613 if (profileKey
!= null) {
614 final var recipientProfileKey
= getProfileKey(recipientId
);
615 if (profileKey
.equals(recipientProfileKey
)) {
616 final var recipientProfile
= getProfile(recipientId
);
617 if (recipientProfile
== null || (
618 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
619 && recipientProfile
.getUnidentifiedAccessMode()
620 != Profile
.UnidentifiedAccessMode
.DISABLED
630 SET profile_key = ?, profile_key_credential = NULL%s
633 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
634 try (final var statement
= connection
.prepareStatement(sql
)) {
635 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
636 statement
.setLong(2, recipientId
.id());
637 statement
.executeUpdate();
641 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
642 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
643 synchronized (recipientsLock
) {
644 try (final var connection
= database
.getConnection()) {
645 connection
.setAutoCommit(false);
646 if (address
.hasSingleIdentifier() || (
647 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
649 pair
= new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
651 pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
653 for (final var toBeMergedRecipientId
: pair
.second()) {
654 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
658 } catch (SQLException e
) {
659 throw new RuntimeException("Failed update recipient store", e
);
663 if (pair
.second().size() > 0) {
664 try (final var connection
= database
.getConnection()) {
665 for (final var toBeMergedRecipientId
: pair
.second()) {
666 recipientMergeHandler
.mergeRecipients(connection
, pair
.first(), toBeMergedRecipientId
);
667 deleteRecipient(connection
, toBeMergedRecipientId
);
669 } catch (SQLException e
) {
670 throw new RuntimeException("Failed update recipient store", e
);
676 private RecipientId
resolveRecipientLocked(
677 Connection connection
, RecipientAddress address
678 ) throws SQLException
{
679 final var byServiceId
= address
.serviceId().isEmpty()
680 ? Optional
.<RecipientWithAddress
>empty()
681 : findByServiceId(connection
, address
.serviceId().get());
683 if (byServiceId
.isPresent()) {
684 return byServiceId
.get().id();
687 final var byPni
= address
.pni().isEmpty()
688 ? Optional
.<RecipientWithAddress
>empty()
689 : findByServiceId(connection
, address
.pni().get());
691 if (byPni
.isPresent()) {
692 return byPni
.get().id();
695 final var byNumber
= address
.number().isEmpty()
696 ? Optional
.<RecipientWithAddress
>empty()
697 : findByNumber(connection
, address
.number().get());
699 if (byNumber
.isPresent()) {
700 return byNumber
.get().id();
703 logger
.debug("Got new recipient, both serviceId and number are unknown");
705 if (address
.serviceId().isEmpty()) {
706 return addNewRecipient(connection
, address
);
709 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
712 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
713 final var recipient
= findByServiceId(connection
, serviceId
);
715 if (recipient
.isEmpty()) {
716 logger
.debug("Got new recipient, serviceId is unknown");
717 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
720 return recipient
.get().id();
723 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
724 final var recipient
= findByNumber(connection
, number
);
726 if (recipient
.isEmpty()) {
727 logger
.debug("Got new recipient, number is unknown");
728 return addNewRecipient(connection
, new RecipientAddress(null, number
));
731 return recipient
.get().id();
734 private RecipientId
addNewRecipient(
735 final Connection connection
, final RecipientAddress address
736 ) throws SQLException
{
739 INSERT INTO %s (number, uuid, pni)
742 ).formatted(TABLE_RECIPIENT
);
743 try (final var statement
= connection
.prepareStatement(sql
)) {
744 statement
.setString(1, address
.number().orElse(null));
745 statement
.setBytes(2, address
.serviceId().map(ServiceId
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
746 statement
.setBytes(3, address
.pni().map(PNI
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
747 statement
.executeUpdate();
748 final var generatedKeys
= statement
.getGeneratedKeys();
749 if (generatedKeys
.next()) {
750 final var recipientId
= new RecipientId(generatedKeys
.getLong(1), this);
751 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
754 throw new RuntimeException("Failed to add new recipient to database");
759 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
763 SET number = NULL, uuid = NULL, pni = NULL
766 ).formatted(TABLE_RECIPIENT
);
767 try (final var statement
= connection
.prepareStatement(sql
)) {
768 statement
.setLong(1, recipientId
.id());
769 statement
.executeUpdate();
773 private void updateRecipientAddress(
774 Connection connection
, RecipientId recipientId
, final RecipientAddress address
775 ) throws SQLException
{
779 SET number = ?, uuid = ?, pni = ?, username = ?
782 ).formatted(TABLE_RECIPIENT
);
783 try (final var statement
= connection
.prepareStatement(sql
)) {
784 statement
.setString(1, address
.number().orElse(null));
785 statement
.setBytes(2, address
.serviceId().map(ServiceId
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
786 statement
.setBytes(3, address
.pni().map(PNI
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
787 statement
.setString(4, address
.username().orElse(null));
788 statement
.setLong(5, recipientId
.id());
789 statement
.executeUpdate();
793 private void deleteRecipient(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 statement
.executeUpdate();
806 private void mergeRecipientsLocked(
807 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
808 ) throws SQLException
{
809 final var contact
= getContact(connection
, recipientId
);
810 if (contact
== null) {
811 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
812 storeContact(connection
, recipientId
, toBeMergedContact
);
815 final var profileKey
= getProfileKey(connection
, recipientId
);
816 if (profileKey
== null) {
817 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
818 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
821 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
822 if (profileKeyCredential
== null) {
823 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
824 toBeMergedRecipientId
);
825 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
828 final var profile
= getProfile(connection
, recipientId
);
829 if (profile
== null) {
830 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
831 storeProfile(connection
, recipientId
, toBeMergedProfile
);
834 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
837 private Optional
<RecipientWithAddress
> findByNumber(
838 final Connection connection
, final String number
839 ) throws SQLException
{
841 SELECT r._id, r.number, r.uuid, r.pni, r.username
845 """.formatted(TABLE_RECIPIENT
);
846 try (final var statement
= connection
.prepareStatement(sql
)) {
847 statement
.setString(1, number
);
848 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
852 private Optional
<RecipientWithAddress
> findByUsername(
853 final Connection connection
, final String username
854 ) throws SQLException
{
856 SELECT r._id, r.number, r.uuid, r.pni, r.username
860 """.formatted(TABLE_RECIPIENT
);
861 try (final var statement
= connection
.prepareStatement(sql
)) {
862 statement
.setString(1, username
);
863 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
867 private Optional
<RecipientWithAddress
> findByServiceId(
868 final Connection connection
, final ServiceId serviceId
869 ) throws SQLException
{
871 SELECT r._id, r.number, r.uuid, r.pni, r.username
873 WHERE r.uuid = ? OR r.pni = ?
875 """.formatted(TABLE_RECIPIENT
);
876 try (final var statement
= connection
.prepareStatement(sql
)) {
877 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.uuid()));
878 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
882 private Set
<RecipientWithAddress
> findAllByAddress(
883 final Connection connection
, final RecipientAddress address
884 ) throws SQLException
{
886 SELECT r._id, r.number, r.uuid, r.pni, r.username
888 WHERE r.uuid = ?1 OR r.pni = ?1 OR
889 r.uuid = ?2 OR r.pni = ?2 OR
892 """.formatted(TABLE_RECIPIENT
);
893 try (final var statement
= connection
.prepareStatement(sql
)) {
894 statement
.setBytes(1, address
.serviceId().map(ServiceId
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
895 statement
.setBytes(2, address
.pni().map(ServiceId
::uuid
).map(UuidUtil
::toByteArray
).orElse(null));
896 statement
.setString(3, address
.number().orElse(null));
897 statement
.setString(4, address
.username().orElse(null));
898 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
899 .collect(Collectors
.toSet());
903 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
906 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
908 WHERE r._id = ? AND (%s)
910 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
911 try (final var statement
= connection
.prepareStatement(sql
)) {
912 statement
.setLong(1, recipientId
.id());
913 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
917 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
924 ).formatted(TABLE_RECIPIENT
);
925 try (final var statement
= connection
.prepareStatement(sql
)) {
926 statement
.setLong(1, recipientId
.id());
927 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
931 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
932 final Connection connection
, final RecipientId recipientId
933 ) throws SQLException
{
936 SELECT r.profile_key_credential
940 ).formatted(TABLE_RECIPIENT
);
941 try (final var statement
= connection
.prepareStatement(sql
)) {
942 statement
.setLong(1, recipientId
.id());
943 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
948 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
951 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
953 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
955 ).formatted(TABLE_RECIPIENT
);
956 try (final var statement
= connection
.prepareStatement(sql
)) {
957 statement
.setLong(1, recipientId
.id());
958 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
962 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
963 final var serviceId
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(ServiceId
::parseOrNull
);
964 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(PNI
::parseOrNull
);
965 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
966 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
967 return new RecipientAddress(serviceId
, pni
, number
, username
);
970 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
971 return new RecipientId(resultSet
.getLong("_id"), this);
974 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
975 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
976 getRecipientAddressFromResultSet(resultSet
));
979 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
980 return new Recipient(getRecipientIdFromResultSet(resultSet
),
981 getRecipientAddressFromResultSet(resultSet
),
982 getContactFromResultSet(resultSet
),
983 getProfileKeyFromResultSet(resultSet
),
984 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
985 getProfileFromResultSet(resultSet
));
988 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
989 return new Contact(resultSet
.getString("given_name"),
990 resultSet
.getString("family_name"),
991 resultSet
.getString("color"),
992 resultSet
.getInt("expiration_time"),
993 resultSet
.getBoolean("blocked"),
994 resultSet
.getBoolean("archived"),
995 resultSet
.getBoolean("profile_sharing"));
998 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
999 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1000 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1001 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1002 resultSet
.getString("profile_given_name"),
1003 resultSet
.getString("profile_family_name"),
1004 resultSet
.getString("profile_about"),
1005 resultSet
.getString("profile_about_emoji"),
1006 resultSet
.getString("profile_avatar_url_path"),
1007 resultSet
.getBytes("profile_mobile_coin_address"),
1008 profileUnidentifiedAccessMode
== null
1009 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1010 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1011 profileCapabilities
== null
1013 : Arrays
.stream(profileCapabilities
.split(","))
1014 .map(Profile
.Capability
::valueOfOrNull
)
1015 .filter(Objects
::nonNull
)
1016 .collect(Collectors
.toSet()));
1019 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1020 final var profileKey
= resultSet
.getBytes("profile_key");
1022 if (profileKey
== null) {
1026 return new ProfileKey(profileKey
);
1027 } catch (InvalidInputException ignored
) {
1032 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1033 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1035 if (profileKeyCredential
== null) {
1039 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1040 } catch (Throwable ignored
) {
1045 public interface RecipientMergeHandler
{
1047 void mergeRecipients(
1048 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1049 ) throws SQLException
;
1052 private class HelperStore
implements MergeRecipientHelper
.Store
{
1054 private final Connection connection
;
1056 public HelperStore(final Connection connection
) {
1057 this.connection
= connection
;
1061 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1062 return RecipientStore
.this.findAllByAddress(connection
, address
);
1066 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1067 return RecipientStore
.this.addNewRecipient(connection
, address
);
1071 public void updateRecipientAddress(
1072 final RecipientId recipientId
, final RecipientAddress address
1073 ) throws SQLException
{
1074 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1078 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1079 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);