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 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,
74 hidden INTEGER NOT NULL DEFAULT FALSE,
76 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
77 profile_given_name TEXT,
78 profile_family_name TEXT,
80 profile_about_emoji TEXT,
81 profile_avatar_url_path TEXT,
82 profile_mobile_coin_address BLOB,
83 profile_unidentified_access_mode TEXT,
84 profile_capabilities TEXT
90 public RecipientStore(
91 final RecipientMergeHandler recipientMergeHandler
,
92 final SelfAddressProvider selfAddressProvider
,
93 final SelfProfileKeyProvider selfProfileKeyProvider
,
94 final Database database
96 this.recipientMergeHandler
= recipientMergeHandler
;
97 this.selfAddressProvider
= selfAddressProvider
;
98 this.selfProfileKeyProvider
= selfProfileKeyProvider
;
99 this.database
= database
;
102 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
105 SELECT r.number, r.uuid, r.pni, r.username
109 ).formatted(TABLE_RECIPIENT
);
110 try (final var connection
= database
.getConnection()) {
111 try (final var statement
= connection
.prepareStatement(sql
)) {
112 statement
.setLong(1, recipientId
.id());
113 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
115 } catch (SQLException e
) {
116 throw new RuntimeException("Failed read from recipient store", e
);
120 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
125 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
127 ).formatted(TABLE_RECIPIENT
);
128 try (final var connection
= database
.getConnection()) {
129 try (final var statement
= connection
.prepareStatement(sql
)) {
130 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
131 return result
.toList();
134 } catch (SQLException e
) {
135 throw new RuntimeException("Failed read from recipient store", e
);
140 public RecipientId
resolveRecipient(final long rawRecipientId
) {
147 ).formatted(TABLE_RECIPIENT
);
148 try (final var connection
= database
.getConnection()) {
149 try (final var statement
= connection
.prepareStatement(sql
)) {
150 statement
.setLong(1, rawRecipientId
);
151 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
153 } catch (SQLException e
) {
154 throw new RuntimeException("Failed read from recipient store", e
);
159 public RecipientId
resolveRecipient(final String identifier
) {
160 final var serviceId
= ServiceId
.parseOrNull(identifier
);
161 if (serviceId
!= null) {
162 return resolveRecipient(serviceId
);
164 return resolveRecipientByNumber(identifier
);
168 private RecipientId
resolveRecipientByNumber(final String number
) {
169 synchronized (recipientsLock
) {
170 final RecipientId recipientId
;
171 try (final var connection
= database
.getConnection()) {
172 connection
.setAutoCommit(false);
173 recipientId
= resolveRecipientLocked(connection
, number
);
175 } catch (SQLException e
) {
176 throw new RuntimeException("Failed read recipient store", e
);
183 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
184 synchronized (recipientsLock
) {
185 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
186 if (recipientWithAddress
!= null) {
187 return recipientWithAddress
.id();
189 try (final var connection
= database
.getConnection()) {
190 connection
.setAutoCommit(false);
191 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
194 } catch (SQLException e
) {
195 throw new RuntimeException("Failed read recipient store", e
);
201 * Should only be used for recipientIds from the database.
202 * Where the foreign key relations ensure a valid recipientId.
205 public RecipientId
create(final long recipientId
) {
206 return new RecipientId(recipientId
, this);
209 public RecipientId
resolveRecipientByNumber(
210 final String number
, Supplier
<ServiceId
> serviceIdSupplier
211 ) throws UnregisteredRecipientException
{
212 final Optional
<RecipientWithAddress
> byNumber
;
213 try (final var connection
= database
.getConnection()) {
214 byNumber
= findByNumber(connection
, number
);
215 } catch (SQLException e
) {
216 throw new RuntimeException("Failed read from recipient store", e
);
218 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
219 final var serviceId
= serviceIdSupplier
.get();
220 if (serviceId
== null) {
221 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
225 return resolveRecipient(serviceId
);
227 return byNumber
.get().id();
230 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
231 final Optional
<RecipientWithAddress
> byNumber
;
232 try (final var connection
= database
.getConnection()) {
233 byNumber
= findByNumber(connection
, number
);
234 } catch (SQLException e
) {
235 throw new RuntimeException("Failed read from recipient store", e
);
237 return byNumber
.map(RecipientWithAddress
::id
);
240 public RecipientId
resolveRecipientByUsername(
241 final String username
, Supplier
<ACI
> aciSupplier
242 ) throws UnregisteredRecipientException
{
243 final Optional
<RecipientWithAddress
> byUsername
;
244 try (final var connection
= database
.getConnection()) {
245 byUsername
= findByUsername(connection
, username
);
246 } catch (SQLException e
) {
247 throw new RuntimeException("Failed read from recipient store", e
);
249 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
250 final var aci
= aciSupplier
.get();
252 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
257 return resolveRecipientTrusted(aci
, username
);
259 return byUsername
.get().id();
262 public RecipientId
resolveRecipient(RecipientAddress address
) {
263 synchronized (recipientsLock
) {
264 final RecipientId recipientId
;
265 try (final var connection
= database
.getConnection()) {
266 connection
.setAutoCommit(false);
267 recipientId
= resolveRecipientLocked(connection
, address
);
269 } catch (SQLException e
) {
270 throw new RuntimeException("Failed read recipient store", e
);
277 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
278 return resolveRecipientTrusted(address
, true);
282 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
283 return resolveRecipientTrusted(address
, false);
287 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
288 return resolveRecipientTrusted(new RecipientAddress(address
));
292 public RecipientId
resolveRecipientTrusted(
293 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
295 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
296 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()));
300 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
301 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
));
305 public void storeContact(RecipientId recipientId
, final Contact contact
) {
306 try (final var connection
= database
.getConnection()) {
307 storeContact(connection
, recipientId
, contact
);
308 } catch (SQLException e
) {
309 throw new RuntimeException("Failed update recipient store", e
);
314 public Contact
getContact(RecipientId recipientId
) {
315 try (final var connection
= database
.getConnection()) {
316 return getContact(connection
, recipientId
);
317 } catch (SQLException e
) {
318 throw new RuntimeException("Failed read from recipient store", e
);
323 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
326 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
328 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s AND r.hidden = FALSE
330 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
331 try (final var connection
= database
.getConnection()) {
332 try (final var statement
= connection
.prepareStatement(sql
)) {
333 try (var result
= Utils
.executeQueryForStream(statement
,
334 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
335 getContactFromResultSet(resultSet
)))) {
336 return result
.toList();
339 } catch (SQLException e
) {
340 throw new RuntimeException("Failed read from recipient store", e
);
344 public List
<Recipient
> getRecipients(
345 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
347 final var sqlWhere
= new ArrayList
<String
>();
349 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
350 sqlWhere
.add("r.hidden = FALSE");
352 if (blocked
.isPresent()) {
353 sqlWhere
.add("r.blocked = ?");
355 if (!recipientIds
.isEmpty()) {
356 final var recipientIdsCommaSeparated
= recipientIds
.stream()
357 .map(recipientId
-> String
.valueOf(recipientId
.id()))
358 .collect(Collectors
.joining(","));
359 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
364 r.number, r.uuid, r.pni, r.username,
365 r.profile_key, r.profile_key_credential,
366 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden,
367 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
369 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
371 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
372 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId();
373 try (final var connection
= database
.getConnection()) {
374 try (final var statement
= connection
.prepareStatement(sql
)) {
375 if (blocked
.isPresent()) {
376 statement
.setBoolean(1, blocked
.get());
378 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
379 return result
.filter(r
-> name
.isEmpty() || (
380 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
381 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).map(r
-> {
382 if (r
.getAddress().serviceId().equals(selfServiceId
)) {
383 return Recipient
.newBuilder(r
)
384 .withProfileKey(selfProfileKeyProvider
.getSelfProfileKey())
391 } catch (SQLException e
) {
392 throw new RuntimeException("Failed read from recipient store", e
);
396 public Set
<String
> getAllNumbers() {
401 WHERE r.number IS NOT NULL
403 ).formatted(TABLE_RECIPIENT
);
404 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
405 try (final var connection
= database
.getConnection()) {
406 try (final var statement
= connection
.prepareStatement(sql
)) {
407 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
408 .filter(Objects
::nonNull
)
409 .filter(n
-> !n
.equals(selfNumber
))
414 } catch (NumberFormatException e
) {
418 .collect(Collectors
.toSet());
420 } catch (SQLException e
) {
421 throw new RuntimeException("Failed read from recipient store", e
);
425 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
428 SELECT r.uuid, r.profile_key
430 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
432 ).formatted(TABLE_RECIPIENT
);
433 final var selfServiceId
= selfAddressProvider
.getSelfAddress().serviceId().orElse(null);
434 try (final var connection
= database
.getConnection()) {
435 try (final var statement
= connection
.prepareStatement(sql
)) {
436 return Utils
.executeQueryForStream(statement
, resultSet
-> {
437 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
438 if (serviceId
.equals(selfServiceId
)) {
439 return new Pair
<>(serviceId
, selfProfileKeyProvider
.getSelfProfileKey());
441 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
442 return new Pair
<>(serviceId
, profileKey
);
443 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
445 } catch (SQLException e
) {
446 throw new RuntimeException("Failed read from recipient store", e
);
451 public void deleteContact(RecipientId recipientId
) {
452 storeContact(recipientId
, null);
455 public void deleteRecipientData(RecipientId recipientId
) {
456 logger
.debug("Deleting recipient data for {}", recipientId
);
457 synchronized (recipientsLock
) {
458 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
459 try (final var connection
= database
.getConnection()) {
460 connection
.setAutoCommit(false);
461 storeContact(connection
, recipientId
, null);
462 storeProfile(connection
, recipientId
, null);
463 storeProfileKey(connection
, recipientId
, null, false);
464 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
465 deleteRecipient(connection
, recipientId
);
467 } catch (SQLException e
) {
468 throw new RuntimeException("Failed update recipient store", e
);
474 public Profile
getProfile(final RecipientId recipientId
) {
475 try (final var connection
= database
.getConnection()) {
476 return getProfile(connection
, recipientId
);
477 } catch (SQLException e
) {
478 throw new RuntimeException("Failed read from recipient store", e
);
483 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
484 final var selfRecipientId
= resolveRecipient(selfAddressProvider
.getSelfAddress());
485 if (recipientId
.equals(selfRecipientId
)) {
486 return selfProfileKeyProvider
.getSelfProfileKey();
488 try (final var connection
= database
.getConnection()) {
489 return getProfileKey(connection
, recipientId
);
490 } catch (SQLException e
) {
491 throw new RuntimeException("Failed read from recipient store", e
);
496 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
497 try (final var connection
= database
.getConnection()) {
498 return getExpiringProfileKeyCredential(connection
, recipientId
);
499 } catch (SQLException e
) {
500 throw new RuntimeException("Failed read from recipient store", e
);
505 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
506 try (final var connection
= database
.getConnection()) {
507 storeProfile(connection
, recipientId
, profile
);
508 } catch (SQLException e
) {
509 throw new RuntimeException("Failed update recipient store", e
);
514 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
515 try (final var connection
= database
.getConnection()) {
516 storeProfileKey(connection
, recipientId
, profileKey
, true);
517 } catch (SQLException e
) {
518 throw new RuntimeException("Failed update recipient store", e
);
523 public void storeExpiringProfileKeyCredential(
524 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
526 try (final var connection
= database
.getConnection()) {
527 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
528 } catch (SQLException e
) {
529 throw new RuntimeException("Failed update recipient store", e
);
533 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
534 logger
.debug("Migrating legacy recipients to database");
535 long start
= System
.nanoTime();
538 INSERT INTO %s (_id, number, uuid)
541 ).formatted(TABLE_RECIPIENT
);
542 try (final var connection
= database
.getConnection()) {
543 connection
.setAutoCommit(false);
544 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
545 statement
.executeUpdate();
547 try (final var statement
= connection
.prepareStatement(sql
)) {
548 for (final var recipient
: recipients
.values()) {
549 statement
.setLong(1, recipient
.getRecipientId().id());
550 statement
.setString(2, recipient
.getAddress().number().orElse(null));
551 statement
.setBytes(3,
552 recipient
.getAddress()
554 .map(ServiceId
::getRawUuid
)
555 .map(UuidUtil
::toByteArray
)
557 statement
.executeUpdate();
560 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
562 for (final var recipient
: recipients
.values()) {
563 if (recipient
.getContact() != null) {
564 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
566 if (recipient
.getProfile() != null) {
567 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
569 if (recipient
.getProfileKey() != null) {
570 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
572 if (recipient
.getExpiringProfileKeyCredential() != null) {
573 storeExpiringProfileKeyCredential(connection
,
574 recipient
.getRecipientId(),
575 recipient
.getExpiringProfileKeyCredential());
579 } catch (SQLException e
) {
580 throw new RuntimeException("Failed update recipient store", e
);
582 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
585 long getActualRecipientId(long recipientId
) {
586 while (recipientsMerged
.containsKey(recipientId
)) {
587 final var newRecipientId
= recipientsMerged
.get(recipientId
);
588 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
589 recipientId
= newRecipientId
;
594 private void storeContact(
595 final Connection connection
, final RecipientId recipientId
, final Contact contact
596 ) throws SQLException
{
600 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
603 ).formatted(TABLE_RECIPIENT
);
604 try (final var statement
= connection
.prepareStatement(sql
)) {
605 statement
.setString(1, contact
== null ?
null : contact
.givenName());
606 statement
.setString(2, contact
== null ?
null : contact
.familyName());
607 statement
.setInt(3, contact
== null ?
0 : contact
.messageExpirationTime());
608 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
609 statement
.setString(5, contact
== null ?
null : contact
.color());
610 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
611 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
612 statement
.setLong(8, recipientId
.id());
613 statement
.executeUpdate();
617 private void storeExpiringProfileKeyCredential(
618 final Connection connection
,
619 final RecipientId recipientId
,
620 final ExpiringProfileKeyCredential profileKeyCredential
621 ) throws SQLException
{
625 SET profile_key_credential = ?
628 ).formatted(TABLE_RECIPIENT
);
629 try (final var statement
= connection
.prepareStatement(sql
)) {
630 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
631 statement
.setLong(2, recipientId
.id());
632 statement
.executeUpdate();
636 private void storeProfile(
637 final Connection connection
, final RecipientId recipientId
, final Profile profile
638 ) throws SQLException
{
642 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 = ?
645 ).formatted(TABLE_RECIPIENT
);
646 try (final var statement
= connection
.prepareStatement(sql
)) {
647 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
648 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
649 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
650 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
651 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
652 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
653 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
654 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
655 statement
.setString(9,
658 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
659 statement
.setLong(10, recipientId
.id());
660 statement
.executeUpdate();
664 private void storeProfileKey(
665 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
666 ) throws SQLException
{
667 if (profileKey
!= null) {
668 final var recipientProfileKey
= getProfileKey(recipientId
);
669 if (profileKey
.equals(recipientProfileKey
)) {
670 final var recipientProfile
= getProfile(recipientId
);
671 if (recipientProfile
== null || (
672 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
673 && recipientProfile
.getUnidentifiedAccessMode()
674 != Profile
.UnidentifiedAccessMode
.DISABLED
684 SET profile_key = ?, profile_key_credential = NULL%s
687 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
688 try (final var statement
= connection
.prepareStatement(sql
)) {
689 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
690 statement
.setLong(2, recipientId
.id());
691 statement
.executeUpdate();
695 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
696 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
697 synchronized (recipientsLock
) {
698 try (final var connection
= database
.getConnection()) {
699 connection
.setAutoCommit(false);
700 if (address
.hasSingleIdentifier() || (
701 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
703 pair
= new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
705 pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
707 for (final var toBeMergedRecipientId
: pair
.second()) {
708 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
712 } catch (SQLException e
) {
713 throw new RuntimeException("Failed update recipient store", e
);
717 if (!pair
.second().isEmpty()) {
718 try (final var connection
= database
.getConnection()) {
719 for (final var toBeMergedRecipientId
: pair
.second()) {
720 recipientMergeHandler
.mergeRecipients(connection
, pair
.first(), toBeMergedRecipientId
);
721 deleteRecipient(connection
, toBeMergedRecipientId
);
722 synchronized (recipientsLock
) {
723 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
726 } catch (SQLException e
) {
727 throw new RuntimeException("Failed update recipient store", e
);
733 private RecipientId
resolveRecipientLocked(
734 Connection connection
, RecipientAddress address
735 ) throws SQLException
{
736 final var byServiceId
= address
.serviceId().isEmpty()
737 ? Optional
.<RecipientWithAddress
>empty()
738 : findByServiceId(connection
, address
.serviceId().get());
740 if (byServiceId
.isPresent()) {
741 return byServiceId
.get().id();
744 final var byPni
= address
.pni().isEmpty()
745 ? Optional
.<RecipientWithAddress
>empty()
746 : findByServiceId(connection
, address
.pni().get());
748 if (byPni
.isPresent()) {
749 return byPni
.get().id();
752 final var byNumber
= address
.number().isEmpty()
753 ? Optional
.<RecipientWithAddress
>empty()
754 : findByNumber(connection
, address
.number().get());
756 if (byNumber
.isPresent()) {
757 return byNumber
.get().id();
760 logger
.debug("Got new recipient, both serviceId and number are unknown");
762 if (address
.serviceId().isEmpty()) {
763 return addNewRecipient(connection
, address
);
766 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
769 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
770 final var recipient
= findByServiceId(connection
, serviceId
);
772 if (recipient
.isEmpty()) {
773 logger
.debug("Got new recipient, serviceId is unknown");
774 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
777 return recipient
.get().id();
780 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
781 final var recipient
= findByNumber(connection
, number
);
783 if (recipient
.isEmpty()) {
784 logger
.debug("Got new recipient, number is unknown");
785 return addNewRecipient(connection
, new RecipientAddress(null, number
));
788 return recipient
.get().id();
791 private RecipientId
addNewRecipient(
792 final Connection connection
, final RecipientAddress address
793 ) throws SQLException
{
796 INSERT INTO %s (number, uuid, pni)
800 ).formatted(TABLE_RECIPIENT
);
801 try (final var statement
= connection
.prepareStatement(sql
)) {
802 statement
.setString(1, address
.number().orElse(null));
803 statement
.setBytes(2,
804 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
805 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
806 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
807 if (generatedKey
.isPresent()) {
808 final var recipientId
= new RecipientId(generatedKey
.get(), this);
809 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
812 throw new RuntimeException("Failed to add new recipient to database");
817 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
818 synchronized (recipientsLock
) {
819 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
823 SET number = NULL, uuid = NULL, pni = NULL
826 ).formatted(TABLE_RECIPIENT
);
827 try (final var statement
= connection
.prepareStatement(sql
)) {
828 statement
.setLong(1, recipientId
.id());
829 statement
.executeUpdate();
834 private void updateRecipientAddress(
835 Connection connection
, RecipientId recipientId
, final RecipientAddress address
836 ) throws SQLException
{
837 synchronized (recipientsLock
) {
838 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
842 SET number = ?, uuid = ?, pni = ?, username = ?
845 ).formatted(TABLE_RECIPIENT
);
846 try (final var statement
= connection
.prepareStatement(sql
)) {
847 statement
.setString(1, address
.number().orElse(null));
848 statement
.setBytes(2,
849 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
850 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
851 statement
.setString(4, address
.username().orElse(null));
852 statement
.setLong(5, recipientId
.id());
853 statement
.executeUpdate();
858 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
864 ).formatted(TABLE_RECIPIENT
);
865 try (final var statement
= connection
.prepareStatement(sql
)) {
866 statement
.setLong(1, recipientId
.id());
867 statement
.executeUpdate();
871 private void mergeRecipientsLocked(
872 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
873 ) throws SQLException
{
874 final var contact
= getContact(connection
, recipientId
);
875 if (contact
== null) {
876 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
877 storeContact(connection
, recipientId
, toBeMergedContact
);
880 final var profileKey
= getProfileKey(connection
, recipientId
);
881 if (profileKey
== null) {
882 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
883 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
886 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
887 if (profileKeyCredential
== null) {
888 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
889 toBeMergedRecipientId
);
890 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
893 final var profile
= getProfile(connection
, recipientId
);
894 if (profile
== null) {
895 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
896 storeProfile(connection
, recipientId
, toBeMergedProfile
);
899 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
902 private Optional
<RecipientWithAddress
> findByNumber(
903 final Connection connection
, final String number
904 ) throws SQLException
{
906 SELECT r._id, r.number, r.uuid, r.pni, r.username
910 """.formatted(TABLE_RECIPIENT
);
911 try (final var statement
= connection
.prepareStatement(sql
)) {
912 statement
.setString(1, number
);
913 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
917 private Optional
<RecipientWithAddress
> findByUsername(
918 final Connection connection
, final String username
919 ) throws SQLException
{
921 SELECT r._id, r.number, r.uuid, r.pni, r.username
925 """.formatted(TABLE_RECIPIENT
);
926 try (final var statement
= connection
.prepareStatement(sql
)) {
927 statement
.setString(1, username
);
928 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
932 private Optional
<RecipientWithAddress
> findByServiceId(
933 final Connection connection
, final ServiceId serviceId
934 ) throws SQLException
{
935 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
936 if (recipientWithAddress
.isPresent()) {
937 return recipientWithAddress
;
940 SELECT r._id, r.number, r.uuid, r.pni, r.username
942 WHERE r.uuid = ?1 OR r.pni = ?1
944 """.formatted(TABLE_RECIPIENT
);
945 try (final var statement
= connection
.prepareStatement(sql
)) {
946 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.getRawUuid()));
947 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
948 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
949 return recipientWithAddress
;
953 private Set
<RecipientWithAddress
> findAllByAddress(
954 final Connection connection
, final RecipientAddress address
955 ) throws SQLException
{
957 SELECT r._id, r.number, r.uuid, r.pni, r.username
959 WHERE r.uuid = ?1 OR r.pni = ?1 OR
960 r.uuid = ?2 OR r.pni = ?2 OR
963 """.formatted(TABLE_RECIPIENT
);
964 try (final var statement
= connection
.prepareStatement(sql
)) {
965 statement
.setBytes(1,
966 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
967 statement
.setBytes(2, address
.pni().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
968 statement
.setString(3, address
.number().orElse(null));
969 statement
.setString(4, address
.username().orElse(null));
970 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
971 .collect(Collectors
.toSet());
975 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
978 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden
980 WHERE r._id = ? AND (%s)
982 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
983 try (final var statement
= connection
.prepareStatement(sql
)) {
984 statement
.setLong(1, recipientId
.id());
985 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
989 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
996 ).formatted(TABLE_RECIPIENT
);
997 try (final var statement
= connection
.prepareStatement(sql
)) {
998 statement
.setLong(1, recipientId
.id());
999 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
1003 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
1004 final Connection connection
, final RecipientId recipientId
1005 ) throws SQLException
{
1008 SELECT r.profile_key_credential
1012 ).formatted(TABLE_RECIPIENT
);
1013 try (final var statement
= connection
.prepareStatement(sql
)) {
1014 statement
.setLong(1, recipientId
.id());
1015 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1020 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1023 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
1025 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1027 ).formatted(TABLE_RECIPIENT
);
1028 try (final var statement
= connection
.prepareStatement(sql
)) {
1029 statement
.setLong(1, recipientId
.id());
1030 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1034 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1035 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(UuidUtil
::parseOrNull
).map(PNI
::from
);
1036 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
1037 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
1038 .equals(pni
.get().getRawUuid()) ? pni
.<ServiceId
>map(p
-> p
) : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
1039 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1040 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1041 return new RecipientAddress(serviceId
, pni
, number
, username
);
1044 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1045 return new RecipientId(resultSet
.getLong("_id"), this);
1048 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1049 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1050 getRecipientAddressFromResultSet(resultSet
));
1053 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1054 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1055 getRecipientAddressFromResultSet(resultSet
),
1056 getContactFromResultSet(resultSet
),
1057 getProfileKeyFromResultSet(resultSet
),
1058 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1059 getProfileFromResultSet(resultSet
));
1062 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1063 return new Contact(resultSet
.getString("given_name"),
1064 resultSet
.getString("family_name"),
1065 resultSet
.getString("color"),
1066 resultSet
.getInt("expiration_time"),
1067 resultSet
.getBoolean("blocked"),
1068 resultSet
.getBoolean("archived"),
1069 resultSet
.getBoolean("profile_sharing"),
1070 resultSet
.getBoolean("hidden"));
1073 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1074 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1075 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1076 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1077 resultSet
.getString("profile_given_name"),
1078 resultSet
.getString("profile_family_name"),
1079 resultSet
.getString("profile_about"),
1080 resultSet
.getString("profile_about_emoji"),
1081 resultSet
.getString("profile_avatar_url_path"),
1082 resultSet
.getBytes("profile_mobile_coin_address"),
1083 profileUnidentifiedAccessMode
== null
1084 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1085 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1086 profileCapabilities
== null
1088 : Arrays
.stream(profileCapabilities
.split(","))
1089 .map(Profile
.Capability
::valueOfOrNull
)
1090 .filter(Objects
::nonNull
)
1091 .collect(Collectors
.toSet()));
1094 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1095 final var profileKey
= resultSet
.getBytes("profile_key");
1097 if (profileKey
== null) {
1101 return new ProfileKey(profileKey
);
1102 } catch (InvalidInputException ignored
) {
1107 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1108 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1110 if (profileKeyCredential
== null) {
1114 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1115 } catch (Throwable ignored
) {
1120 public interface RecipientMergeHandler
{
1122 void mergeRecipients(
1123 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1124 ) throws SQLException
;
1127 private class HelperStore
implements MergeRecipientHelper
.Store
{
1129 private final Connection connection
;
1131 public HelperStore(final Connection connection
) {
1132 this.connection
= connection
;
1136 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1137 return RecipientStore
.this.findAllByAddress(connection
, address
);
1141 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1142 return RecipientStore
.this.addNewRecipient(connection
, address
);
1146 public void updateRecipientAddress(
1147 final RecipientId recipientId
, final RecipientAddress address
1148 ) throws SQLException
{
1149 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1153 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1154 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);