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 final static 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 SelfProfileKeyProvider selfProfileKeyProvider
;
46 private final Database database
;
48 private final Object recipientsLock
= new Object();
49 private final Map
<Long
, Long
> recipientsMerged
= new HashMap
<>();
51 private final Map
<ServiceId
, RecipientWithAddress
> recipientAddressCache
= new HashMap
<>();
53 public static void createSql(Connection connection
) throws SQLException
{
54 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
55 try (final var statement
= connection
.createStatement()) {
56 statement
.executeUpdate("""
57 CREATE TABLE recipient (
58 _id INTEGER PRIMARY KEY AUTOINCREMENT,
64 profile_key_credential BLOB,
70 expiration_time INTEGER NOT NULL DEFAULT 0,
71 blocked INTEGER NOT NULL DEFAULT FALSE,
72 archived INTEGER NOT NULL DEFAULT FALSE,
73 profile_sharing 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 SelfProfileKeyProvider selfProfileKeyProvider
,
93 final Database database
95 this.recipientMergeHandler
= recipientMergeHandler
;
96 this.selfAddressProvider
= selfAddressProvider
;
97 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
98 this.database
= database
;
101 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
104 SELECT r.number, r.uuid, r.pni, r.username
108 ).formatted(TABLE_RECIPIENT
);
109 try (final var connection
= database
.getConnection()) {
110 try (final var statement
= connection
.prepareStatement(sql
)) {
111 statement
.setLong(1, recipientId
.id());
112 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
114 } catch (SQLException e
) {
115 throw new RuntimeException("Failed read from recipient store", e
);
119 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
124 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
126 ).formatted(TABLE_RECIPIENT
);
127 try (final var connection
= database
.getConnection()) {
128 try (final var statement
= connection
.prepareStatement(sql
)) {
129 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
130 return result
.toList();
133 } catch (SQLException e
) {
134 throw new RuntimeException("Failed read from recipient store", e
);
139 public RecipientId
resolveRecipient(final long rawRecipientId
) {
146 ).formatted(TABLE_RECIPIENT
);
147 try (final var connection
= database
.getConnection()) {
148 try (final var statement
= connection
.prepareStatement(sql
)) {
149 statement
.setLong(1, rawRecipientId
);
150 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
152 } catch (SQLException e
) {
153 throw new RuntimeException("Failed read from recipient store", e
);
158 public RecipientId
resolveRecipient(final String identifier
) {
159 final var serviceId
= ServiceId
.parseOrNull(identifier
);
160 if (serviceId
!= null) {
161 return resolveRecipient(serviceId
);
163 return resolveRecipientByNumber(identifier
);
167 private RecipientId
resolveRecipientByNumber(final String number
) {
168 synchronized (recipientsLock
) {
169 final RecipientId recipientId
;
170 try (final var connection
= database
.getConnection()) {
171 connection
.setAutoCommit(false);
172 recipientId
= resolveRecipientLocked(connection
, number
);
174 } catch (SQLException e
) {
175 throw new RuntimeException("Failed read recipient store", e
);
182 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
183 synchronized (recipientsLock
) {
184 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
185 if (recipientWithAddress
!= null) {
186 return recipientWithAddress
.id();
188 try (final var connection
= database
.getConnection()) {
189 connection
.setAutoCommit(false);
190 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
193 } catch (SQLException e
) {
194 throw new RuntimeException("Failed read recipient store", e
);
200 * Should only be used for recipientIds from the database.
201 * Where the foreign key relations ensure a valid recipientId.
204 public RecipientId
create(final long recipientId
) {
205 return new RecipientId(recipientId
, this);
208 public RecipientId
resolveRecipientByNumber(
209 final String number
, Supplier
<ServiceId
> serviceIdSupplier
210 ) throws UnregisteredRecipientException
{
211 final Optional
<RecipientWithAddress
> byNumber
;
212 try (final var connection
= database
.getConnection()) {
213 byNumber
= findByNumber(connection
, number
);
214 } catch (SQLException e
) {
215 throw new RuntimeException("Failed read from recipient store", e
);
217 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
218 final var serviceId
= serviceIdSupplier
.get();
219 if (serviceId
== null) {
220 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
224 return resolveRecipient(serviceId
);
226 return byNumber
.get().id();
229 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
230 final Optional
<RecipientWithAddress
> byNumber
;
231 try (final var connection
= database
.getConnection()) {
232 byNumber
= findByNumber(connection
, number
);
233 } catch (SQLException e
) {
234 throw new RuntimeException("Failed read from recipient store", e
);
236 return byNumber
.map(RecipientWithAddress
::id
);
239 public RecipientId
resolveRecipientByUsername(
240 final String username
, Supplier
<ACI
> aciSupplier
241 ) throws UnregisteredRecipientException
{
242 final Optional
<RecipientWithAddress
> byUsername
;
243 try (final var connection
= database
.getConnection()) {
244 byUsername
= findByUsername(connection
, username
);
245 } catch (SQLException e
) {
246 throw new RuntimeException("Failed read from recipient store", e
);
248 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
249 final var aci
= aciSupplier
.get();
251 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
256 return resolveRecipientTrusted(aci
, username
);
258 return byUsername
.get().id();
261 public RecipientId
resolveRecipient(RecipientAddress address
) {
262 synchronized (recipientsLock
) {
263 final RecipientId recipientId
;
264 try (final var connection
= database
.getConnection()) {
265 connection
.setAutoCommit(false);
266 recipientId
= resolveRecipientLocked(connection
, address
);
268 } catch (SQLException e
) {
269 throw new RuntimeException("Failed read recipient store", e
);
276 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
277 return resolveRecipientTrusted(address
, true);
280 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
281 return resolveRecipientTrusted(address
, false);
285 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
286 return resolveRecipientTrusted(new RecipientAddress(address
), false);
290 public RecipientId
resolveRecipientTrusted(
291 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
293 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
294 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()), false);
298 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
299 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
), false);
303 public void storeContact(RecipientId recipientId
, final Contact contact
) {
304 try (final var connection
= database
.getConnection()) {
305 storeContact(connection
, recipientId
, contact
);
306 } catch (SQLException e
) {
307 throw new RuntimeException("Failed update recipient store", e
);
312 public Contact
getContact(RecipientId recipientId
) {
313 try (final var connection
= database
.getConnection()) {
314 return getContact(connection
, recipientId
);
315 } catch (SQLException e
) {
316 throw new RuntimeException("Failed read from recipient store", e
);
321 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
324 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
326 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
328 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
329 try (final var connection
= database
.getConnection()) {
330 try (final var statement
= connection
.prepareStatement(sql
)) {
331 try (var result
= Utils
.executeQueryForStream(statement
,
332 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
333 getContactFromResultSet(resultSet
)))) {
334 return result
.toList();
337 } catch (SQLException e
) {
338 throw new RuntimeException("Failed read from recipient store", e
);
342 public List
<Recipient
> getRecipients(
343 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
345 final var sqlWhere
= new ArrayList
<String
>();
347 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
349 if (blocked
.isPresent()) {
350 sqlWhere
.add("r.blocked = ?");
352 if (!recipientIds
.isEmpty()) {
353 final var recipientIdsCommaSeparated
= recipientIds
.stream()
354 .map(recipientId
-> String
.valueOf(recipientId
.id()))
355 .collect(Collectors
.joining(","));
356 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
361 r.number, r.uuid, r.pni, r.username,
362 r.profile_key, r.profile_key_credential,
363 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
364 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
366 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
368 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
369 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId();
370 try (final var connection
= database
.getConnection()) {
371 try (final var statement
= connection
.prepareStatement(sql
)) {
372 if (blocked
.isPresent()) {
373 statement
.setBoolean(1, blocked
.get());
375 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
376 return result
.filter(r
-> name
.isEmpty() || (
377 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
378 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
379 if (r
.getAddress().serviceId().equals(selfServiceId
)) {
380 return Recipient
.newBuilder(r
)
381 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
388 } catch (SQLException e
) {
389 throw new RuntimeException("Failed read from recipient store", e
);
393 public Set
<String
> getAllNumbers() {
398 WHERE r.number IS NOT NULL
400 ).formatted(TABLE_RECIPIENT
);
401 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
402 try (final var connection
= database
.getConnection()) {
403 try (final var statement
= connection
.prepareStatement(sql
)) {
404 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
405 .filter(Objects
::nonNull
)
406 .filter(n
-> !n
.equals(selfNumber
))
411 } catch (NumberFormatException e
) {
415 .collect(Collectors
.toSet());
417 } catch (SQLException e
) {
418 throw new RuntimeException("Failed read from recipient store", e
);
422 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
425 SELECT r.uuid, r.profile_key
427 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
429 ).formatted(TABLE_RECIPIENT
);
430 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId().orElse(null);
431 try (final var connection
= database
.getConnection()) {
432 try (final var statement
= connection
.prepareStatement(sql
)) {
433 return Utils
.executeQueryForStream(statement
, resultSet
-> {
434 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
435 if (serviceId
.equals(selfServiceId
)) {
436 return new Pair
<>(serviceId
, selfProfileKeyProvider
.getSelfProfileKey());
438 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
439 return new Pair
<>(serviceId
, profileKey
);
440 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
442 } catch (SQLException e
) {
443 throw new RuntimeException("Failed read from recipient store", e
);
448 public void deleteContact(RecipientId recipientId
) {
449 storeContact(recipientId
, null);
452 public void deleteRecipientData(RecipientId recipientId
) {
453 logger
.debug("Deleting recipient data for {}", recipientId
);
454 synchronized (recipientsLock
) {
455 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
456 try (final var connection
= database
.getConnection()) {
457 connection
.setAutoCommit(false);
458 storeContact(connection
, recipientId
, null);
459 storeProfile(connection
, recipientId
, null);
460 storeProfileKey(connection
, recipientId
, null, false);
461 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
462 deleteRecipient(connection
, recipientId
);
464 } catch (SQLException e
) {
465 throw new RuntimeException("Failed update recipient store", e
);
471 public Profile
getProfile(final RecipientId recipientId
) {
472 try (final var connection
= database
.getConnection()) {
473 return getProfile(connection
, recipientId
);
474 } catch (SQLException e
) {
475 throw new RuntimeException("Failed read from recipient store", e
);
480 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
481 final var selfRecipientId
= resolveRecipient(selfAddressProvider
.getSelfAddress());
482 if (recipientId
.equals(selfRecipientId
)) {
483 return selfProfileKeyProvider
.getSelfProfileKey();
485 try (final var connection
= database
.getConnection()) {
486 return getProfileKey(connection
, recipientId
);
487 } catch (SQLException e
) {
488 throw new RuntimeException("Failed read from recipient store", e
);
493 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
494 try (final var connection
= database
.getConnection()) {
495 return getExpiringProfileKeyCredential(connection
, recipientId
);
496 } catch (SQLException e
) {
497 throw new RuntimeException("Failed read from recipient store", e
);
502 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
503 try (final var connection
= database
.getConnection()) {
504 storeProfile(connection
, recipientId
, profile
);
505 } catch (SQLException e
) {
506 throw new RuntimeException("Failed update recipient store", e
);
511 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
512 try (final var connection
= database
.getConnection()) {
513 storeProfileKey(connection
, recipientId
, profileKey
, true);
514 } catch (SQLException e
) {
515 throw new RuntimeException("Failed update recipient store", e
);
520 public void storeExpiringProfileKeyCredential(
521 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
523 try (final var connection
= database
.getConnection()) {
524 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
525 } catch (SQLException e
) {
526 throw new RuntimeException("Failed update recipient store", e
);
530 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
531 logger
.debug("Migrating legacy recipients to database");
532 long start
= System
.nanoTime();
535 INSERT INTO %s (_id, number, uuid)
538 ).formatted(TABLE_RECIPIENT
);
539 try (final var connection
= database
.getConnection()) {
540 connection
.setAutoCommit(false);
541 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
542 statement
.executeUpdate();
544 try (final var statement
= connection
.prepareStatement(sql
)) {
545 for (final var recipient
: recipients
.values()) {
546 statement
.setLong(1, recipient
.getRecipientId().id());
547 statement
.setString(2, recipient
.getAddress().number().orElse(null));
548 statement
.setBytes(3,
549 recipient
.getAddress()
551 .map(ServiceId
::getRawUuid
)
552 .map(UuidUtil
::toByteArray
)
554 statement
.executeUpdate();
557 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
559 for (final var recipient
: recipients
.values()) {
560 if (recipient
.getContact() != null) {
561 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
563 if (recipient
.getProfile() != null) {
564 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
566 if (recipient
.getProfileKey() != null) {
567 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
569 if (recipient
.getExpiringProfileKeyCredential() != null) {
570 storeExpiringProfileKeyCredential(connection
,
571 recipient
.getRecipientId(),
572 recipient
.getExpiringProfileKeyCredential());
576 } catch (SQLException e
) {
577 throw new RuntimeException("Failed update recipient store", e
);
579 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
582 long getActualRecipientId(long recipientId
) {
583 while (recipientsMerged
.containsKey(recipientId
)) {
584 final var newRecipientId
= recipientsMerged
.get(recipientId
);
585 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
586 recipientId
= newRecipientId
;
591 private void storeContact(
592 final Connection connection
, final RecipientId recipientId
, final Contact contact
593 ) throws SQLException
{
597 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
600 ).formatted(TABLE_RECIPIENT
);
601 try (final var statement
= connection
.prepareStatement(sql
)) {
602 statement
.setString(1, contact
== null ?
null : contact
.getGivenName());
603 statement
.setString(2, contact
== null ?
null : contact
.getFamilyName());
604 statement
.setInt(3, contact
== null ?
0 : contact
.getMessageExpirationTime());
605 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
606 statement
.setString(5, contact
== null ?
null : contact
.getColor());
607 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
608 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
609 statement
.setLong(8, recipientId
.id());
610 statement
.executeUpdate();
614 private void storeExpiringProfileKeyCredential(
615 final Connection connection
,
616 final RecipientId recipientId
,
617 final ExpiringProfileKeyCredential profileKeyCredential
618 ) throws SQLException
{
622 SET profile_key_credential = ?
625 ).formatted(TABLE_RECIPIENT
);
626 try (final var statement
= connection
.prepareStatement(sql
)) {
627 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
628 statement
.setLong(2, recipientId
.id());
629 statement
.executeUpdate();
633 private void storeProfile(
634 final Connection connection
, final RecipientId recipientId
, final Profile profile
635 ) throws SQLException
{
639 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 = ?
642 ).formatted(TABLE_RECIPIENT
);
643 try (final var statement
= connection
.prepareStatement(sql
)) {
644 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
645 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
646 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
647 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
648 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
649 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
650 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
651 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
652 statement
.setString(9,
655 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
656 statement
.setLong(10, recipientId
.id());
657 statement
.executeUpdate();
661 private void storeProfileKey(
662 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
663 ) throws SQLException
{
664 if (profileKey
!= null) {
665 final var recipientProfileKey
= getProfileKey(recipientId
);
666 if (profileKey
.equals(recipientProfileKey
)) {
667 final var recipientProfile
= getProfile(recipientId
);
668 if (recipientProfile
== null || (
669 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
670 && recipientProfile
.getUnidentifiedAccessMode()
671 != Profile
.UnidentifiedAccessMode
.DISABLED
681 SET profile_key = ?, profile_key_credential = NULL%s
684 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
685 try (final var statement
= connection
.prepareStatement(sql
)) {
686 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
687 statement
.setLong(2, recipientId
.id());
688 statement
.executeUpdate();
692 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
693 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
694 synchronized (recipientsLock
) {
695 try (final var connection
= database
.getConnection()) {
696 connection
.setAutoCommit(false);
697 if (address
.hasSingleIdentifier() || (
698 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
700 pair
= new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
702 pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
704 for (final var toBeMergedRecipientId
: pair
.second()) {
705 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
709 } catch (SQLException e
) {
710 throw new RuntimeException("Failed update recipient store", e
);
714 if (!pair
.second().isEmpty()) {
715 try (final var connection
= database
.getConnection()) {
716 for (final var toBeMergedRecipientId
: pair
.second()) {
717 recipientMergeHandler
.mergeRecipients(connection
, pair
.first(), toBeMergedRecipientId
);
718 deleteRecipient(connection
, toBeMergedRecipientId
);
719 synchronized (recipientsLock
) {
720 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
723 } catch (SQLException e
) {
724 throw new RuntimeException("Failed update recipient store", e
);
730 private RecipientId
resolveRecipientLocked(
731 Connection connection
, RecipientAddress address
732 ) throws SQLException
{
733 final var byServiceId
= address
.serviceId().isEmpty()
734 ? Optional
.<RecipientWithAddress
>empty()
735 : findByServiceId(connection
, address
.serviceId().get());
737 if (byServiceId
.isPresent()) {
738 return byServiceId
.get().id();
741 final var byPni
= address
.pni().isEmpty()
742 ? Optional
.<RecipientWithAddress
>empty()
743 : findByServiceId(connection
, address
.pni().get());
745 if (byPni
.isPresent()) {
746 return byPni
.get().id();
749 final var byNumber
= address
.number().isEmpty()
750 ? Optional
.<RecipientWithAddress
>empty()
751 : findByNumber(connection
, address
.number().get());
753 if (byNumber
.isPresent()) {
754 return byNumber
.get().id();
757 logger
.debug("Got new recipient, both serviceId and number are unknown");
759 if (address
.serviceId().isEmpty()) {
760 return addNewRecipient(connection
, address
);
763 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
766 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
767 final var recipient
= findByServiceId(connection
, serviceId
);
769 if (recipient
.isEmpty()) {
770 logger
.debug("Got new recipient, serviceId is unknown");
771 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
774 return recipient
.get().id();
777 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
778 final var recipient
= findByNumber(connection
, number
);
780 if (recipient
.isEmpty()) {
781 logger
.debug("Got new recipient, number is unknown");
782 return addNewRecipient(connection
, new RecipientAddress(null, number
));
785 return recipient
.get().id();
788 private RecipientId
addNewRecipient(
789 final Connection connection
, final RecipientAddress address
790 ) throws SQLException
{
793 INSERT INTO %s (number, uuid, pni, username)
797 ).formatted(TABLE_RECIPIENT
);
798 try (final var statement
= connection
.prepareStatement(sql
)) {
799 statement
.setString(1, address
.number().orElse(null));
800 statement
.setBytes(2,
801 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
802 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
803 statement
.setString(4, address
.username().orElse(null));
804 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
805 if (generatedKey
.isPresent()) {
806 final var recipientId
= new RecipientId(generatedKey
.get(), this);
807 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
810 throw new RuntimeException("Failed to add new recipient to database");
815 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
816 synchronized (recipientsLock
) {
817 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
821 SET number = NULL, uuid = NULL, pni = NULL, username = NULL
824 ).formatted(TABLE_RECIPIENT
);
825 try (final var statement
= connection
.prepareStatement(sql
)) {
826 statement
.setLong(1, recipientId
.id());
827 statement
.executeUpdate();
832 private void updateRecipientAddress(
833 Connection connection
, RecipientId recipientId
, final RecipientAddress address
834 ) throws SQLException
{
835 synchronized (recipientsLock
) {
836 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
840 SET number = ?, uuid = ?, pni = ?, username = ?
843 ).formatted(TABLE_RECIPIENT
);
844 try (final var statement
= connection
.prepareStatement(sql
)) {
845 statement
.setString(1, address
.number().orElse(null));
846 statement
.setBytes(2,
847 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
848 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
849 statement
.setString(4, address
.username().orElse(null));
850 statement
.setLong(5, recipientId
.id());
851 statement
.executeUpdate();
856 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
862 ).formatted(TABLE_RECIPIENT
);
863 try (final var statement
= connection
.prepareStatement(sql
)) {
864 statement
.setLong(1, recipientId
.id());
865 statement
.executeUpdate();
869 private void mergeRecipientsLocked(
870 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
871 ) throws SQLException
{
872 final var contact
= getContact(connection
, recipientId
);
873 if (contact
== null) {
874 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
875 storeContact(connection
, recipientId
, toBeMergedContact
);
878 final var profileKey
= getProfileKey(connection
, recipientId
);
879 if (profileKey
== null) {
880 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
881 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
884 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
885 if (profileKeyCredential
== null) {
886 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
887 toBeMergedRecipientId
);
888 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
891 final var profile
= getProfile(connection
, recipientId
);
892 if (profile
== null) {
893 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
894 storeProfile(connection
, recipientId
, toBeMergedProfile
);
897 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
900 private Optional
<RecipientWithAddress
> findByNumber(
901 final Connection connection
, final String number
902 ) throws SQLException
{
904 SELECT r._id, r.number, r.uuid, r.pni, r.username
908 """.formatted(TABLE_RECIPIENT
);
909 try (final var statement
= connection
.prepareStatement(sql
)) {
910 statement
.setString(1, number
);
911 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
915 private Optional
<RecipientWithAddress
> findByUsername(
916 final Connection connection
, final String username
917 ) throws SQLException
{
919 SELECT r._id, r.number, r.uuid, r.pni, r.username
923 """.formatted(TABLE_RECIPIENT
);
924 try (final var statement
= connection
.prepareStatement(sql
)) {
925 statement
.setString(1, username
);
926 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
930 private Optional
<RecipientWithAddress
> findByServiceId(
931 final Connection connection
, final ServiceId serviceId
932 ) throws SQLException
{
933 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
934 if (recipientWithAddress
.isPresent()) {
935 return recipientWithAddress
;
938 SELECT r._id, r.number, r.uuid, r.pni, r.username
940 WHERE r.uuid = ?1 OR r.pni = ?1
942 """.formatted(TABLE_RECIPIENT
);
943 try (final var statement
= connection
.prepareStatement(sql
)) {
944 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.getRawUuid()));
945 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
946 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
947 return recipientWithAddress
;
951 private Set
<RecipientWithAddress
> findAllByAddress(
952 final Connection connection
, final RecipientAddress address
953 ) throws SQLException
{
955 SELECT r._id, r.number, r.uuid, r.pni, r.username
957 WHERE r.uuid = ?1 OR r.pni = ?1 OR
958 r.uuid = ?2 OR r.pni = ?2 OR
961 """.formatted(TABLE_RECIPIENT
);
962 try (final var statement
= connection
.prepareStatement(sql
)) {
963 statement
.setBytes(1,
964 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
965 statement
.setBytes(2, address
.pni().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
966 statement
.setString(3, address
.number().orElse(null));
967 statement
.setString(4, address
.username().orElse(null));
968 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
969 .collect(Collectors
.toSet());
973 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
976 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
978 WHERE r._id = ? AND (%s)
980 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
981 try (final var statement
= connection
.prepareStatement(sql
)) {
982 statement
.setLong(1, recipientId
.id());
983 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
987 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
994 ).formatted(TABLE_RECIPIENT
);
995 try (final var statement
= connection
.prepareStatement(sql
)) {
996 statement
.setLong(1, recipientId
.id());
997 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1001 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1002 final Connection connection
, final RecipientId recipientId
1003 ) throws SQLException
{
1006 SELECT r.profile_key_credential
1010 ).formatted(TABLE_RECIPIENT
);
1011 try (final var statement
= connection
.prepareStatement(sql
)) {
1012 statement
.setLong(1, recipientId
.id());
1013 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1018 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1021 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
1023 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1025 ).formatted(TABLE_RECIPIENT
);
1026 try (final var statement
= connection
.prepareStatement(sql
)) {
1027 statement
.setLong(1, recipientId
.id());
1028 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1032 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1033 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(UuidUtil
::parseOrNull
).map(PNI
::from
);
1034 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
1035 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
1036 .equals(pni
.get().getRawUuid()) ? pni
.<ServiceId
>map(p
-> p
) : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
1037 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1038 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1039 return new RecipientAddress(serviceId
, pni
, number
, username
);
1042 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1043 return new RecipientId(resultSet
.getLong("_id"), this);
1046 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1047 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1048 getRecipientAddressFromResultSet(resultSet
));
1051 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1052 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1053 getRecipientAddressFromResultSet(resultSet
),
1054 getContactFromResultSet(resultSet
),
1055 getProfileKeyFromResultSet(resultSet
),
1056 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1057 getProfileFromResultSet(resultSet
));
1060 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1061 return new Contact(resultSet
.getString("given_name"),
1062 resultSet
.getString("family_name"),
1063 resultSet
.getString("color"),
1064 resultSet
.getInt("expiration_time"),
1065 resultSet
.getBoolean("blocked"),
1066 resultSet
.getBoolean("archived"),
1067 resultSet
.getBoolean("profile_sharing"));
1070 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1071 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1072 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1073 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1074 resultSet
.getString("profile_given_name"),
1075 resultSet
.getString("profile_family_name"),
1076 resultSet
.getString("profile_about"),
1077 resultSet
.getString("profile_about_emoji"),
1078 resultSet
.getString("profile_avatar_url_path"),
1079 resultSet
.getBytes("profile_mobile_coin_address"),
1080 profileUnidentifiedAccessMode
== null
1081 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1082 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1083 profileCapabilities
== null
1085 : Arrays
.stream(profileCapabilities
.split(","))
1086 .map(Profile
.Capability
::valueOfOrNull
)
1087 .filter(Objects
::nonNull
)
1088 .collect(Collectors
.toSet()));
1091 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1092 final var profileKey
= resultSet
.getBytes("profile_key");
1094 if (profileKey
== null) {
1098 return new ProfileKey(profileKey
);
1099 } catch (InvalidInputException ignored
) {
1104 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1105 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1107 if (profileKeyCredential
== null) {
1111 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1112 } catch (Throwable ignored
) {
1117 public interface RecipientMergeHandler
{
1119 void mergeRecipients(
1120 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1121 ) throws SQLException
;
1124 private class HelperStore
implements MergeRecipientHelper
.Store
{
1126 private final Connection connection
;
1128 public HelperStore(final Connection connection
) {
1129 this.connection
= connection
;
1133 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1134 return RecipientStore
.this.findAllByAddress(connection
, address
);
1138 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1139 return RecipientStore
.this.addNewRecipient(connection
, address
);
1143 public void updateRecipientAddress(
1144 final RecipientId recipientId
, final RecipientAddress address
1145 ) throws SQLException
{
1146 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1150 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1151 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);