1 package org
.asamk
.signal
.manager
.storage
.recipients
;
3 import org
.asamk
.signal
.manager
.api
.Contact
;
4 import org
.asamk
.signal
.manager
.api
.Pair
;
5 import org
.asamk
.signal
.manager
.api
.Profile
;
6 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
7 import org
.asamk
.signal
.manager
.storage
.Database
;
8 import org
.asamk
.signal
.manager
.storage
.Utils
;
9 import org
.asamk
.signal
.manager
.storage
.contacts
.ContactsStore
;
10 import org
.asamk
.signal
.manager
.storage
.profiles
.ProfileStore
;
11 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
12 import org
.signal
.libsignal
.zkgroup
.profiles
.ExpiringProfileKeyCredential
;
13 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
14 import org
.slf4j
.Logger
;
15 import org
.slf4j
.LoggerFactory
;
16 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
17 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
18 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
19 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
20 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
22 import java
.sql
.Connection
;
23 import java
.sql
.ResultSet
;
24 import java
.sql
.SQLException
;
25 import java
.util
.ArrayList
;
26 import java
.util
.Arrays
;
27 import java
.util
.Collection
;
28 import java
.util
.HashMap
;
29 import java
.util
.List
;
31 import java
.util
.Objects
;
32 import java
.util
.Optional
;
34 import java
.util
.function
.Supplier
;
35 import java
.util
.stream
.Collectors
;
37 public class RecipientStore
implements RecipientIdCreator
, RecipientResolver
, RecipientTrustedResolver
, ContactsStore
, ProfileStore
{
39 private static final Logger logger
= LoggerFactory
.getLogger(RecipientStore
.class);
40 private static final String TABLE_RECIPIENT
= "recipient";
41 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";
43 private final RecipientMergeHandler recipientMergeHandler
;
44 private final SelfAddressProvider selfAddressProvider
;
45 private final Database database
;
47 private final Object recipientsLock
= new Object();
48 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
50 private final Map
<ServiceId
, RecipientWithAddress
> recipientAddressCache
= new HashMap
<>();
52 public static void createSql(Connection connection
) throws SQLException
{
53 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
54 try (final var statement
= connection
.createStatement()) {
55 statement
.executeUpdate("""
56 CREATE TABLE recipient (
57 _id INTEGER PRIMARY KEY AUTOINCREMENT,
63 profile_key_credential BLOB,
69 expiration_time INTEGER NOT NULL DEFAULT 0,
70 blocked INTEGER NOT NULL DEFAULT FALSE,
71 archived INTEGER NOT NULL DEFAULT FALSE,
72 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
73 hidden INTEGER NOT NULL DEFAULT FALSE,
75 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
76 profile_given_name TEXT,
77 profile_family_name TEXT,
79 profile_about_emoji TEXT,
80 profile_avatar_url_path TEXT,
81 profile_mobile_coin_address BLOB,
82 profile_unidentified_access_mode TEXT,
83 profile_capabilities TEXT
89 public RecipientStore(
90 final RecipientMergeHandler recipientMergeHandler
,
91 final SelfAddressProvider selfAddressProvider
,
92 final Database database
94 this.recipientMergeHandler
= recipientMergeHandler
;
95 this.selfAddressProvider
= selfAddressProvider
;
96 this.database
= database
;
99 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
102 SELECT r.number, r.uuid, r.pni, r.username
106 ).formatted(TABLE_RECIPIENT
);
107 try (final var connection
= database
.getConnection()) {
108 try (final var statement
= connection
.prepareStatement(sql
)) {
109 statement
.setLong(1, recipientId
.id());
110 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
112 } catch (SQLException e
) {
113 throw new RuntimeException("Failed read from recipient store", e
);
117 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
122 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
124 ).formatted(TABLE_RECIPIENT
);
125 try (final var connection
= database
.getConnection()) {
126 try (final var statement
= connection
.prepareStatement(sql
)) {
127 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
128 return result
.toList();
131 } catch (SQLException e
) {
132 throw new RuntimeException("Failed read from recipient store", e
);
137 public RecipientId
resolveRecipient(final long rawRecipientId
) {
144 ).formatted(TABLE_RECIPIENT
);
145 try (final var connection
= database
.getConnection()) {
146 try (final var statement
= connection
.prepareStatement(sql
)) {
147 statement
.setLong(1, rawRecipientId
);
148 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
150 } catch (SQLException e
) {
151 throw new RuntimeException("Failed read from recipient store", e
);
156 public RecipientId
resolveRecipient(final String identifier
) {
157 final var serviceId
= ServiceId
.parseOrNull(identifier
);
158 if (serviceId
!= null) {
159 return resolveRecipient(serviceId
);
161 return resolveRecipientByNumber(identifier
);
165 private RecipientId
resolveRecipientByNumber(final String number
) {
166 synchronized (recipientsLock
) {
167 final RecipientId recipientId
;
168 try (final var connection
= database
.getConnection()) {
169 connection
.setAutoCommit(false);
170 recipientId
= resolveRecipientLocked(connection
, number
);
172 } catch (SQLException e
) {
173 throw new RuntimeException("Failed read recipient store", e
);
180 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
181 synchronized (recipientsLock
) {
182 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
183 if (recipientWithAddress
!= null) {
184 return recipientWithAddress
.id();
186 try (final var connection
= database
.getConnection()) {
187 connection
.setAutoCommit(false);
188 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
191 } catch (SQLException e
) {
192 throw new RuntimeException("Failed read recipient store", e
);
198 * Should only be used for recipientIds from the database.
199 * Where the foreign key relations ensure a valid recipientId.
202 public RecipientId
create(final long recipientId
) {
203 return new RecipientId(recipientId
, this);
206 public RecipientId
resolveRecipientByNumber(
207 final String number
, Supplier
<ServiceId
> serviceIdSupplier
208 ) throws UnregisteredRecipientException
{
209 final Optional
<RecipientWithAddress
> byNumber
;
210 try (final var connection
= database
.getConnection()) {
211 byNumber
= findByNumber(connection
, number
);
212 } catch (SQLException e
) {
213 throw new RuntimeException("Failed read from recipient store", e
);
215 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
216 final var serviceId
= serviceIdSupplier
.get();
217 if (serviceId
== null) {
218 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
222 return resolveRecipient(serviceId
);
224 return byNumber
.get().id();
227 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
228 final Optional
<RecipientWithAddress
> byNumber
;
229 try (final var connection
= database
.getConnection()) {
230 byNumber
= findByNumber(connection
, number
);
231 } catch (SQLException e
) {
232 throw new RuntimeException("Failed read from recipient store", e
);
234 return byNumber
.map(RecipientWithAddress
::id
);
237 public RecipientId
resolveRecipientByUsername(
238 final String username
, Supplier
<ACI
> aciSupplier
239 ) throws UnregisteredRecipientException
{
240 final Optional
<RecipientWithAddress
> byUsername
;
241 try (final var connection
= database
.getConnection()) {
242 byUsername
= findByUsername(connection
, username
);
243 } catch (SQLException e
) {
244 throw new RuntimeException("Failed read from recipient store", e
);
246 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
247 final var aci
= aciSupplier
.get();
249 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
254 return resolveRecipientTrusted(aci
, username
);
256 return byUsername
.get().id();
259 public RecipientId
resolveRecipient(RecipientAddress address
) {
260 synchronized (recipientsLock
) {
261 final RecipientId recipientId
;
262 try (final var connection
= database
.getConnection()) {
263 connection
.setAutoCommit(false);
264 recipientId
= resolveRecipientLocked(connection
, address
);
266 } catch (SQLException e
) {
267 throw new RuntimeException("Failed read recipient store", e
);
274 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
275 return resolveRecipientTrusted(address
, true);
278 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
279 return resolveRecipientTrusted(address
, false);
283 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
284 return resolveRecipientTrusted(new RecipientAddress(address
), false);
288 public RecipientId
resolveRecipientTrusted(
289 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
291 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
292 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()), false);
296 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
297 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
), false);
301 public void storeContact(RecipientId recipientId
, final Contact contact
) {
302 try (final var connection
= database
.getConnection()) {
303 storeContact(connection
, recipientId
, contact
);
304 } catch (SQLException e
) {
305 throw new RuntimeException("Failed update recipient store", e
);
310 public Contact
getContact(RecipientId recipientId
) {
311 try (final var connection
= database
.getConnection()) {
312 return getContact(connection
, recipientId
);
313 } catch (SQLException e
) {
314 throw new RuntimeException("Failed read from recipient store", e
);
319 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
322 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
324 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s AND r.hidden = FALSE
326 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
327 try (final var connection
= database
.getConnection()) {
328 try (final var statement
= connection
.prepareStatement(sql
)) {
329 try (var result
= Utils
.executeQueryForStream(statement
,
330 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
331 getContactFromResultSet(resultSet
)))) {
332 return result
.toList();
335 } catch (SQLException e
) {
336 throw new RuntimeException("Failed read from recipient store", e
);
340 public List
<Recipient
> getRecipients(
341 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
343 final var sqlWhere
= new ArrayList
<String
>();
345 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
346 sqlWhere
.add("r.hidden = FALSE");
348 if (blocked
.isPresent()) {
349 sqlWhere
.add("r.blocked = ?");
351 if (!recipientIds
.isEmpty()) {
352 final var recipientIdsCommaSeparated
= recipientIds
.stream()
353 .map(recipientId
-> String
.valueOf(recipientId
.id()))
354 .collect(Collectors
.joining(","));
355 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
360 r.number, r.uuid, r.pni, r.username,
361 r.profile_key, r.profile_key_credential,
362 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
363 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
365 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
367 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
368 try (final var connection
= database
.getConnection()) {
369 try (final var statement
= connection
.prepareStatement(sql
)) {
370 if (blocked
.isPresent()) {
371 statement
.setBoolean(1, blocked
.get());
373 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
374 return result
.filter(r
-> name
.isEmpty() || (
375 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
376 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).toList();
379 } catch (SQLException e
) {
380 throw new RuntimeException("Failed read from recipient store", e
);
384 public Set
<String
> getAllNumbers() {
389 WHERE r.number IS NOT NULL
391 ).formatted(TABLE_RECIPIENT
);
392 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
393 try (final var connection
= database
.getConnection()) {
394 try (final var statement
= connection
.prepareStatement(sql
)) {
395 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
396 .filter(Objects
::nonNull
)
397 .filter(n
-> !n
.equals(selfNumber
))
402 } catch (NumberFormatException e
) {
406 .collect(Collectors
.toSet());
408 } catch (SQLException e
) {
409 throw new RuntimeException("Failed read from recipient store", e
);
413 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
416 SELECT r.uuid, r.profile_key
418 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
420 ).formatted(TABLE_RECIPIENT
);
421 try (final var connection
= database
.getConnection()) {
422 try (final var statement
= connection
.prepareStatement(sql
)) {
423 return Utils
.executeQueryForStream(statement
, resultSet
-> {
424 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
425 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
426 return new Pair
<>(serviceId
, profileKey
);
427 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
429 } catch (SQLException e
) {
430 throw new RuntimeException("Failed read from recipient store", e
);
435 public void deleteContact(RecipientId recipientId
) {
436 storeContact(recipientId
, null);
439 public void deleteRecipientData(RecipientId recipientId
) {
440 logger
.debug("Deleting recipient data for {}", recipientId
);
441 synchronized (recipientsLock
) {
442 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
443 try (final var connection
= database
.getConnection()) {
444 connection
.setAutoCommit(false);
445 storeContact(connection
, recipientId
, null);
446 storeProfile(connection
, recipientId
, null);
447 storeProfileKey(connection
, recipientId
, null, false);
448 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
449 deleteRecipient(connection
, recipientId
);
451 } catch (SQLException e
) {
452 throw new RuntimeException("Failed update recipient store", e
);
458 public Profile
getProfile(final RecipientId recipientId
) {
459 try (final var connection
= database
.getConnection()) {
460 return getProfile(connection
, recipientId
);
461 } catch (SQLException e
) {
462 throw new RuntimeException("Failed read from recipient store", e
);
467 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
468 try (final var connection
= database
.getConnection()) {
469 return getProfileKey(connection
, recipientId
);
470 } catch (SQLException e
) {
471 throw new RuntimeException("Failed read from recipient store", e
);
476 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
477 try (final var connection
= database
.getConnection()) {
478 return getExpiringProfileKeyCredential(connection
, recipientId
);
479 } catch (SQLException e
) {
480 throw new RuntimeException("Failed read from recipient store", e
);
485 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
486 try (final var connection
= database
.getConnection()) {
487 storeProfile(connection
, recipientId
, profile
);
488 } catch (SQLException e
) {
489 throw new RuntimeException("Failed update recipient store", e
);
494 public void storeSelfProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
495 try (final var connection
= database
.getConnection()) {
496 storeProfileKey(connection
, recipientId
, profileKey
, false);
497 } catch (SQLException e
) {
498 throw new RuntimeException("Failed update recipient store", e
);
503 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
504 try (final var connection
= database
.getConnection()) {
505 storeProfileKey(connection
, recipientId
, profileKey
, true);
506 } catch (SQLException e
) {
507 throw new RuntimeException("Failed update recipient store", e
);
512 public void storeExpiringProfileKeyCredential(
513 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
515 try (final var connection
= database
.getConnection()) {
516 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
517 } catch (SQLException e
) {
518 throw new RuntimeException("Failed update recipient store", e
);
522 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
523 logger
.debug("Migrating legacy recipients to database");
524 long start
= System
.nanoTime();
527 INSERT INTO %s (_id, number, uuid)
530 ).formatted(TABLE_RECIPIENT
);
531 try (final var connection
= database
.getConnection()) {
532 connection
.setAutoCommit(false);
533 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
534 statement
.executeUpdate();
536 try (final var statement
= connection
.prepareStatement(sql
)) {
537 for (final var recipient
: recipients
.values()) {
538 statement
.setLong(1, recipient
.getRecipientId().id());
539 statement
.setString(2, recipient
.getAddress().number().orElse(null));
540 statement
.setBytes(3,
541 recipient
.getAddress()
543 .map(ServiceId
::getRawUuid
)
544 .map(UuidUtil
::toByteArray
)
546 statement
.executeUpdate();
549 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
551 for (final var recipient
: recipients
.values()) {
552 if (recipient
.getContact() != null) {
553 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
555 if (recipient
.getProfile() != null) {
556 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
558 if (recipient
.getProfileKey() != null) {
559 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
561 if (recipient
.getExpiringProfileKeyCredential() != null) {
562 storeExpiringProfileKeyCredential(connection
,
563 recipient
.getRecipientId(),
564 recipient
.getExpiringProfileKeyCredential());
568 } catch (SQLException e
) {
569 throw new RuntimeException("Failed update recipient store", e
);
571 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
574 long getActualRecipientId(long recipientId
) {
575 while (recipientsMerged
.containsKey(recipientId
)) {
576 final var newRecipientId
= recipientsMerged
.get(recipientId
);
577 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
578 recipientId
= newRecipientId
;
583 private void storeContact(
584 final Connection connection
, final RecipientId recipientId
, final Contact contact
585 ) throws SQLException
{
589 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
592 ).formatted(TABLE_RECIPIENT
);
593 try (final var statement
= connection
.prepareStatement(sql
)) {
594 statement
.setString(1, contact
== null ?
null : contact
.givenName());
595 statement
.setString(2, contact
== null ?
null : contact
.familyName());
596 statement
.setInt(3, contact
== null ?
0 : contact
.messageExpirationTime());
597 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
598 statement
.setString(5, contact
== null ?
null : contact
.color());
599 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
600 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
601 statement
.setLong(8, recipientId
.id());
602 statement
.executeUpdate();
606 private void storeExpiringProfileKeyCredential(
607 final Connection connection
,
608 final RecipientId recipientId
,
609 final ExpiringProfileKeyCredential profileKeyCredential
610 ) throws SQLException
{
614 SET profile_key_credential = ?
617 ).formatted(TABLE_RECIPIENT
);
618 try (final var statement
= connection
.prepareStatement(sql
)) {
619 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
620 statement
.setLong(2, recipientId
.id());
621 statement
.executeUpdate();
625 private void storeProfile(
626 final Connection connection
, final RecipientId recipientId
, final Profile profile
627 ) throws SQLException
{
631 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 = ?
634 ).formatted(TABLE_RECIPIENT
);
635 try (final var statement
= connection
.prepareStatement(sql
)) {
636 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
637 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
638 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
639 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
640 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
641 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
642 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
643 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
644 statement
.setString(9,
647 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
648 statement
.setLong(10, recipientId
.id());
649 statement
.executeUpdate();
653 private void storeProfileKey(
654 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
655 ) throws SQLException
{
656 if (profileKey
!= null) {
657 final var recipientProfileKey
= getProfileKey(recipientId
);
658 if (profileKey
.equals(recipientProfileKey
)) {
659 final var recipientProfile
= getProfile(recipientId
);
660 if (recipientProfile
== null || (
661 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
662 && recipientProfile
.getUnidentifiedAccessMode()
663 != Profile
.UnidentifiedAccessMode
.DISABLED
673 SET profile_key = ?, profile_key_credential = NULL%s
676 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
677 try (final var statement
= connection
.prepareStatement(sql
)) {
678 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
679 statement
.setLong(2, recipientId
.id());
680 statement
.executeUpdate();
684 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
685 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
686 synchronized (recipientsLock
) {
687 try (final var connection
= database
.getConnection()) {
688 connection
.setAutoCommit(false);
689 if (address
.hasSingleIdentifier() || (
690 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
692 pair
= new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
694 pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
696 for (final var toBeMergedRecipientId
: pair
.second()) {
697 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
701 } catch (SQLException e
) {
702 throw new RuntimeException("Failed update recipient store", e
);
706 if (!pair
.second().isEmpty()) {
707 try (final var connection
= database
.getConnection()) {
708 for (final var toBeMergedRecipientId
: pair
.second()) {
709 recipientMergeHandler
.mergeRecipients(connection
, pair
.first(), toBeMergedRecipientId
);
710 deleteRecipient(connection
, toBeMergedRecipientId
);
711 synchronized (recipientsLock
) {
712 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
715 } catch (SQLException e
) {
716 throw new RuntimeException("Failed update recipient store", e
);
722 private RecipientId
resolveRecipientLocked(
723 Connection connection
, RecipientAddress address
724 ) throws SQLException
{
725 final var byServiceId
= address
.serviceId().isEmpty()
726 ? Optional
.<RecipientWithAddress
>empty()
727 : findByServiceId(connection
, address
.serviceId().get());
729 if (byServiceId
.isPresent()) {
730 return byServiceId
.get().id();
733 final var byPni
= address
.pni().isEmpty()
734 ? Optional
.<RecipientWithAddress
>empty()
735 : findByServiceId(connection
, address
.pni().get());
737 if (byPni
.isPresent()) {
738 return byPni
.get().id();
741 final var byNumber
= address
.number().isEmpty()
742 ? Optional
.<RecipientWithAddress
>empty()
743 : findByNumber(connection
, address
.number().get());
745 if (byNumber
.isPresent()) {
746 return byNumber
.get().id();
749 logger
.debug("Got new recipient, both serviceId and number are unknown");
751 if (address
.serviceId().isEmpty()) {
752 return addNewRecipient(connection
, address
);
755 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
758 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
759 final var recipient
= findByServiceId(connection
, serviceId
);
761 if (recipient
.isEmpty()) {
762 logger
.debug("Got new recipient, serviceId is unknown");
763 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
766 return recipient
.get().id();
769 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
770 final var recipient
= findByNumber(connection
, number
);
772 if (recipient
.isEmpty()) {
773 logger
.debug("Got new recipient, number is unknown");
774 return addNewRecipient(connection
, new RecipientAddress(null, number
));
777 return recipient
.get().id();
780 private RecipientId
addNewRecipient(
781 final Connection connection
, final RecipientAddress address
782 ) throws SQLException
{
785 INSERT INTO %s (number, uuid, pni)
789 ).formatted(TABLE_RECIPIENT
);
790 try (final var statement
= connection
.prepareStatement(sql
)) {
791 statement
.setString(1, address
.number().orElse(null));
792 statement
.setBytes(2,
793 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
794 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
795 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
796 if (generatedKey
.isPresent()) {
797 final var recipientId
= new RecipientId(generatedKey
.get(), this);
798 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
801 throw new RuntimeException("Failed to add new recipient to database");
806 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
807 synchronized (recipientsLock
) {
808 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
812 SET number = NULL, uuid = NULL, pni = NULL
815 ).formatted(TABLE_RECIPIENT
);
816 try (final var statement
= connection
.prepareStatement(sql
)) {
817 statement
.setLong(1, recipientId
.id());
818 statement
.executeUpdate();
823 private void updateRecipientAddress(
824 Connection connection
, RecipientId recipientId
, final RecipientAddress address
825 ) throws SQLException
{
826 synchronized (recipientsLock
) {
827 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
831 SET number = ?, uuid = ?, pni = ?, username = ?
834 ).formatted(TABLE_RECIPIENT
);
835 try (final var statement
= connection
.prepareStatement(sql
)) {
836 statement
.setString(1, address
.number().orElse(null));
837 statement
.setBytes(2,
838 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
839 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
840 statement
.setString(4, address
.username().orElse(null));
841 statement
.setLong(5, recipientId
.id());
842 statement
.executeUpdate();
847 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
853 ).formatted(TABLE_RECIPIENT
);
854 try (final var statement
= connection
.prepareStatement(sql
)) {
855 statement
.setLong(1, recipientId
.id());
856 statement
.executeUpdate();
860 private void mergeRecipientsLocked(
861 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
862 ) throws SQLException
{
863 final var contact
= getContact(connection
, recipientId
);
864 if (contact
== null) {
865 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
866 storeContact(connection
, recipientId
, toBeMergedContact
);
869 final var profileKey
= getProfileKey(connection
, recipientId
);
870 if (profileKey
== null) {
871 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
872 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
875 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
876 if (profileKeyCredential
== null) {
877 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
878 toBeMergedRecipientId
);
879 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
882 final var profile
= getProfile(connection
, recipientId
);
883 if (profile
== null) {
884 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
885 storeProfile(connection
, recipientId
, toBeMergedProfile
);
888 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
891 private Optional
<RecipientWithAddress
> findByNumber(
892 final Connection connection
, final String number
893 ) throws SQLException
{
895 SELECT r._id, r.number, r.uuid, r.pni, r.username
899 """.formatted(TABLE_RECIPIENT
);
900 try (final var statement
= connection
.prepareStatement(sql
)) {
901 statement
.setString(1, number
);
902 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
906 private Optional
<RecipientWithAddress
> findByUsername(
907 final Connection connection
, final String username
908 ) throws SQLException
{
910 SELECT r._id, r.number, r.uuid, r.pni, r.username
914 """.formatted(TABLE_RECIPIENT
);
915 try (final var statement
= connection
.prepareStatement(sql
)) {
916 statement
.setString(1, username
);
917 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
921 private Optional
<RecipientWithAddress
> findByServiceId(
922 final Connection connection
, final ServiceId serviceId
923 ) throws SQLException
{
924 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
925 if (recipientWithAddress
.isPresent()) {
926 return recipientWithAddress
;
929 SELECT r._id, r.number, r.uuid, r.pni, r.username
931 WHERE r.uuid = ?1 OR r.pni = ?1
933 """.formatted(TABLE_RECIPIENT
);
934 try (final var statement
= connection
.prepareStatement(sql
)) {
935 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.getRawUuid()));
936 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
937 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
938 return recipientWithAddress
;
942 private Set
<RecipientWithAddress
> findAllByAddress(
943 final Connection connection
, final RecipientAddress address
944 ) throws SQLException
{
946 SELECT r._id, r.number, r.uuid, r.pni, r.username
948 WHERE r.uuid = ?1 OR r.pni = ?1 OR
949 r.uuid = ?2 OR r.pni = ?2 OR
952 """.formatted(TABLE_RECIPIENT
);
953 try (final var statement
= connection
.prepareStatement(sql
)) {
954 statement
.setBytes(1,
955 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
956 statement
.setBytes(2, address
.pni().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
957 statement
.setString(3, address
.number().orElse(null));
958 statement
.setString(4, address
.username().orElse(null));
959 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
960 .collect(Collectors
.toSet());
964 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
967 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
969 WHERE r._id = ? AND (%s)
971 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
972 try (final var statement
= connection
.prepareStatement(sql
)) {
973 statement
.setLong(1, recipientId
.id());
974 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
978 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
985 ).formatted(TABLE_RECIPIENT
);
986 try (final var statement
= connection
.prepareStatement(sql
)) {
987 statement
.setLong(1, recipientId
.id());
988 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
992 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
993 final Connection connection
, final RecipientId recipientId
994 ) throws SQLException
{
997 SELECT r.profile_key_credential
1001 ).formatted(TABLE_RECIPIENT
);
1002 try (final var statement
= connection
.prepareStatement(sql
)) {
1003 statement
.setLong(1, recipientId
.id());
1004 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1009 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1012 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
1014 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1016 ).formatted(TABLE_RECIPIENT
);
1017 try (final var statement
= connection
.prepareStatement(sql
)) {
1018 statement
.setLong(1, recipientId
.id());
1019 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1023 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1024 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(UuidUtil
::parseOrNull
).map(PNI
::from
);
1025 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
1026 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
1027 .equals(pni
.get().getRawUuid()) ? pni
.<ServiceId
>map(p
-> p
) : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
1028 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1029 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1030 return new RecipientAddress(serviceId
, pni
, number
, username
);
1033 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1034 return new RecipientId(resultSet
.getLong("_id"), this);
1037 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1038 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1039 getRecipientAddressFromResultSet(resultSet
));
1042 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1043 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1044 getRecipientAddressFromResultSet(resultSet
),
1045 getContactFromResultSet(resultSet
),
1046 getProfileKeyFromResultSet(resultSet
),
1047 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1048 getProfileFromResultSet(resultSet
));
1051 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1052 return new Contact(resultSet
.getString("given_name"),
1053 resultSet
.getString("family_name"),
1054 resultSet
.getString("color"),
1055 resultSet
.getInt("expiration_time"),
1056 resultSet
.getBoolean("blocked"),
1057 resultSet
.getBoolean("archived"),
1058 resultSet
.getBoolean("profile_sharing"),
1059 resultSet
.getBoolean("hidden"));
1062 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1063 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1064 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1065 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1066 resultSet
.getString("profile_given_name"),
1067 resultSet
.getString("profile_family_name"),
1068 resultSet
.getString("profile_about"),
1069 resultSet
.getString("profile_about_emoji"),
1070 resultSet
.getString("profile_avatar_url_path"),
1071 resultSet
.getBytes("profile_mobile_coin_address"),
1072 profileUnidentifiedAccessMode
== null
1073 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1074 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1075 profileCapabilities
== null
1077 : Arrays
.stream(profileCapabilities
.split(","))
1078 .map(Profile
.Capability
::valueOfOrNull
)
1079 .filter(Objects
::nonNull
)
1080 .collect(Collectors
.toSet()));
1083 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1084 final var profileKey
= resultSet
.getBytes("profile_key");
1086 if (profileKey
== null) {
1090 return new ProfileKey(profileKey
);
1091 } catch (InvalidInputException ignored
) {
1096 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1097 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1099 if (profileKeyCredential
== null) {
1103 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1104 } catch (Throwable ignored
) {
1109 public interface RecipientMergeHandler
{
1111 void mergeRecipients(
1112 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1113 ) throws SQLException
;
1116 private class HelperStore
implements MergeRecipientHelper
.Store
{
1118 private final Connection connection
;
1120 public HelperStore(final Connection connection
) {
1121 this.connection
= connection
;
1125 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1126 return RecipientStore
.this.findAllByAddress(connection
, address
);
1130 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1131 return RecipientStore
.this.addNewRecipient(connection
, address
);
1135 public void updateRecipientAddress(
1136 final RecipientId recipientId
, final RecipientAddress address
1137 ) throws SQLException
{
1138 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1142 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1143 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);