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 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,
74 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
75 profile_given_name TEXT,
76 profile_family_name TEXT,
78 profile_about_emoji TEXT,
79 profile_avatar_url_path TEXT,
80 profile_mobile_coin_address BLOB,
81 profile_unidentified_access_mode TEXT,
82 profile_capabilities TEXT
88 public RecipientStore(
89 final RecipientMergeHandler recipientMergeHandler
,
90 final SelfAddressProvider selfAddressProvider
,
91 final Database database
93 this.recipientMergeHandler
= recipientMergeHandler
;
94 this.selfAddressProvider
= selfAddressProvider
;
95 this.database
= database
;
98 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
101 SELECT r.number, r.uuid, r.pni, r.username
105 ).formatted(TABLE_RECIPIENT
);
106 try (final var connection
= database
.getConnection()) {
107 try (final var statement
= connection
.prepareStatement(sql
)) {
108 statement
.setLong(1, recipientId
.id());
109 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
111 } catch (SQLException e
) {
112 throw new RuntimeException("Failed read from recipient store", e
);
116 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
121 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
123 ).formatted(TABLE_RECIPIENT
);
124 try (final var connection
= database
.getConnection()) {
125 try (final var statement
= connection
.prepareStatement(sql
)) {
126 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
127 return result
.toList();
130 } catch (SQLException e
) {
131 throw new RuntimeException("Failed read from recipient store", e
);
136 public RecipientId
resolveRecipient(final long rawRecipientId
) {
143 ).formatted(TABLE_RECIPIENT
);
144 try (final var connection
= database
.getConnection()) {
145 try (final var statement
= connection
.prepareStatement(sql
)) {
146 statement
.setLong(1, rawRecipientId
);
147 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
149 } catch (SQLException e
) {
150 throw new RuntimeException("Failed read from recipient store", e
);
155 public RecipientId
resolveRecipient(final String identifier
) {
156 final var serviceId
= ServiceId
.parseOrNull(identifier
);
157 if (serviceId
!= null) {
158 return resolveRecipient(serviceId
);
160 return resolveRecipientByNumber(identifier
);
164 private RecipientId
resolveRecipientByNumber(final String number
) {
165 synchronized (recipientsLock
) {
166 final RecipientId recipientId
;
167 try (final var connection
= database
.getConnection()) {
168 connection
.setAutoCommit(false);
169 recipientId
= resolveRecipientLocked(connection
, number
);
171 } catch (SQLException e
) {
172 throw new RuntimeException("Failed read recipient store", e
);
179 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
180 synchronized (recipientsLock
) {
181 final var recipientWithAddress
= recipientAddressCache
.get(serviceId
);
182 if (recipientWithAddress
!= null) {
183 return recipientWithAddress
.id();
185 try (final var connection
= database
.getConnection()) {
186 connection
.setAutoCommit(false);
187 final var recipientId
= resolveRecipientLocked(connection
, serviceId
);
190 } catch (SQLException e
) {
191 throw new RuntimeException("Failed read recipient store", e
);
197 * Should only be used for recipientIds from the database.
198 * Where the foreign key relations ensure a valid recipientId.
201 public RecipientId
create(final long recipientId
) {
202 return new RecipientId(recipientId
, this);
205 public RecipientId
resolveRecipientByNumber(
206 final String number
, Supplier
<ServiceId
> serviceIdSupplier
207 ) throws UnregisteredRecipientException
{
208 final Optional
<RecipientWithAddress
> byNumber
;
209 try (final var connection
= database
.getConnection()) {
210 byNumber
= findByNumber(connection
, number
);
211 } catch (SQLException e
) {
212 throw new RuntimeException("Failed read from recipient store", e
);
214 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
215 final var serviceId
= serviceIdSupplier
.get();
216 if (serviceId
== null) {
217 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
221 return resolveRecipient(serviceId
);
223 return byNumber
.get().id();
226 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
227 final Optional
<RecipientWithAddress
> byNumber
;
228 try (final var connection
= database
.getConnection()) {
229 byNumber
= findByNumber(connection
, number
);
230 } catch (SQLException e
) {
231 throw new RuntimeException("Failed read from recipient store", e
);
233 return byNumber
.map(RecipientWithAddress
::id
);
236 public RecipientId
resolveRecipientByUsername(
237 final String username
, Supplier
<ACI
> aciSupplier
238 ) throws UnregisteredRecipientException
{
239 final Optional
<RecipientWithAddress
> byUsername
;
240 try (final var connection
= database
.getConnection()) {
241 byUsername
= findByUsername(connection
, username
);
242 } catch (SQLException e
) {
243 throw new RuntimeException("Failed read from recipient store", e
);
245 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
246 final var aci
= aciSupplier
.get();
248 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
253 return resolveRecipientTrusted(aci
, username
);
255 return byUsername
.get().id();
258 public RecipientId
resolveRecipient(RecipientAddress address
) {
259 synchronized (recipientsLock
) {
260 final RecipientId recipientId
;
261 try (final var connection
= database
.getConnection()) {
262 connection
.setAutoCommit(false);
263 recipientId
= resolveRecipientLocked(connection
, address
);
265 } catch (SQLException e
) {
266 throw new RuntimeException("Failed read recipient store", e
);
273 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
274 return resolveRecipientTrusted(address
, true);
277 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
278 return resolveRecipientTrusted(address
, false);
282 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
283 return resolveRecipientTrusted(new RecipientAddress(address
), false);
287 public RecipientId
resolveRecipientTrusted(
288 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
290 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
291 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()), false);
295 public RecipientId
resolveRecipientTrusted(final ACI aci
, final String username
) {
296 return resolveRecipientTrusted(new RecipientAddress(aci
, null, null, username
), false);
300 public void storeContact(RecipientId recipientId
, final Contact contact
) {
301 try (final var connection
= database
.getConnection()) {
302 storeContact(connection
, recipientId
, contact
);
303 } catch (SQLException e
) {
304 throw new RuntimeException("Failed update recipient store", e
);
309 public Contact
getContact(RecipientId recipientId
) {
310 try (final var connection
= database
.getConnection()) {
311 return getContact(connection
, recipientId
);
312 } catch (SQLException e
) {
313 throw new RuntimeException("Failed read from recipient store", e
);
318 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
321 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
323 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
325 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
326 try (final var connection
= database
.getConnection()) {
327 try (final var statement
= connection
.prepareStatement(sql
)) {
328 try (var result
= Utils
.executeQueryForStream(statement
,
329 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
330 getContactFromResultSet(resultSet
)))) {
331 return result
.toList();
334 } catch (SQLException e
) {
335 throw new RuntimeException("Failed read from recipient store", e
);
339 public List
<Recipient
> getRecipients(
340 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
342 final var sqlWhere
= new ArrayList
<String
>();
344 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
346 if (blocked
.isPresent()) {
347 sqlWhere
.add("r.blocked = ?");
349 if (!recipientIds
.isEmpty()) {
350 final var recipientIdsCommaSeparated
= recipientIds
.stream()
351 .map(recipientId
-> String
.valueOf(recipientId
.id()))
352 .collect(Collectors
.joining(","));
353 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
358 r.number, r.uuid, r.pni, r.username,
359 r.profile_key, r.profile_key_credential,
360 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
361 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
363 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
365 ).formatted(TABLE_RECIPIENT
, sqlWhere
.isEmpty() ?
"TRUE" : String
.join(" AND ", sqlWhere
));
366 try (final var connection
= database
.getConnection()) {
367 try (final var statement
= connection
.prepareStatement(sql
)) {
368 if (blocked
.isPresent()) {
369 statement
.setBoolean(1, blocked
.get());
371 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientFromResultSet
)) {
372 return result
.filter(r
-> name
.isEmpty() || (
373 r
.getContact() != null && name
.get().equals(r
.getContact().getName())
374 ) || (r
.getProfile() != null && name
.get().equals(r
.getProfile().getDisplayName()))).toList();
377 } catch (SQLException e
) {
378 throw new RuntimeException("Failed read from recipient store", e
);
382 public Set
<String
> getAllNumbers() {
387 WHERE r.number IS NOT NULL
389 ).formatted(TABLE_RECIPIENT
);
390 final var selfNumber
= selfAddressProvider
.getSelfAddress().number().orElse(null);
391 try (final var connection
= database
.getConnection()) {
392 try (final var statement
= connection
.prepareStatement(sql
)) {
393 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
394 .filter(Objects
::nonNull
)
395 .filter(n
-> !n
.equals(selfNumber
))
400 } catch (NumberFormatException e
) {
404 .collect(Collectors
.toSet());
406 } catch (SQLException e
) {
407 throw new RuntimeException("Failed read from recipient store", e
);
411 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
414 SELECT r.uuid, r.profile_key
416 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
418 ).formatted(TABLE_RECIPIENT
);
419 try (final var connection
= database
.getConnection()) {
420 try (final var statement
= connection
.prepareStatement(sql
)) {
421 return Utils
.executeQueryForStream(statement
, resultSet
-> {
422 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
423 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
424 return new Pair
<>(serviceId
, profileKey
);
425 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
427 } catch (SQLException e
) {
428 throw new RuntimeException("Failed read from recipient store", e
);
433 public void deleteContact(RecipientId recipientId
) {
434 storeContact(recipientId
, null);
437 public void deleteRecipientData(RecipientId recipientId
) {
438 logger
.debug("Deleting recipient data for {}", recipientId
);
439 synchronized (recipientsLock
) {
440 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
441 try (final var connection
= database
.getConnection()) {
442 connection
.setAutoCommit(false);
443 storeContact(connection
, recipientId
, null);
444 storeProfile(connection
, recipientId
, null);
445 storeProfileKey(connection
, recipientId
, null, false);
446 storeExpiringProfileKeyCredential(connection
, recipientId
, null);
447 deleteRecipient(connection
, recipientId
);
449 } catch (SQLException e
) {
450 throw new RuntimeException("Failed update recipient store", e
);
456 public Profile
getProfile(final RecipientId recipientId
) {
457 try (final var connection
= database
.getConnection()) {
458 return getProfile(connection
, recipientId
);
459 } catch (SQLException e
) {
460 throw new RuntimeException("Failed read from recipient store", e
);
465 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
466 try (final var connection
= database
.getConnection()) {
467 return getProfileKey(connection
, recipientId
);
468 } catch (SQLException e
) {
469 throw new RuntimeException("Failed read from recipient store", e
);
474 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
475 try (final var connection
= database
.getConnection()) {
476 return getExpiringProfileKeyCredential(connection
, recipientId
);
477 } catch (SQLException e
) {
478 throw new RuntimeException("Failed read from recipient store", e
);
483 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
484 try (final var connection
= database
.getConnection()) {
485 storeProfile(connection
, recipientId
, profile
);
486 } catch (SQLException e
) {
487 throw new RuntimeException("Failed update recipient store", e
);
492 public void storeSelfProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
493 try (final var connection
= database
.getConnection()) {
494 storeProfileKey(connection
, recipientId
, profileKey
, false);
495 } catch (SQLException e
) {
496 throw new RuntimeException("Failed update recipient store", e
);
501 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
502 try (final var connection
= database
.getConnection()) {
503 storeProfileKey(connection
, recipientId
, profileKey
, true);
504 } catch (SQLException e
) {
505 throw new RuntimeException("Failed update recipient store", e
);
510 public void storeExpiringProfileKeyCredential(
511 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
513 try (final var connection
= database
.getConnection()) {
514 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
515 } catch (SQLException e
) {
516 throw new RuntimeException("Failed update recipient store", e
);
520 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
521 logger
.debug("Migrating legacy recipients to database");
522 long start
= System
.nanoTime();
525 INSERT INTO %s (_id, number, uuid)
528 ).formatted(TABLE_RECIPIENT
);
529 try (final var connection
= database
.getConnection()) {
530 connection
.setAutoCommit(false);
531 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
532 statement
.executeUpdate();
534 try (final var statement
= connection
.prepareStatement(sql
)) {
535 for (final var recipient
: recipients
.values()) {
536 statement
.setLong(1, recipient
.getRecipientId().id());
537 statement
.setString(2, recipient
.getAddress().number().orElse(null));
538 statement
.setBytes(3,
539 recipient
.getAddress()
541 .map(ServiceId
::getRawUuid
)
542 .map(UuidUtil
::toByteArray
)
544 statement
.executeUpdate();
547 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
549 for (final var recipient
: recipients
.values()) {
550 if (recipient
.getContact() != null) {
551 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
553 if (recipient
.getProfile() != null) {
554 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
556 if (recipient
.getProfileKey() != null) {
557 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
559 if (recipient
.getExpiringProfileKeyCredential() != null) {
560 storeExpiringProfileKeyCredential(connection
,
561 recipient
.getRecipientId(),
562 recipient
.getExpiringProfileKeyCredential());
566 } catch (SQLException e
) {
567 throw new RuntimeException("Failed update recipient store", e
);
569 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
572 long getActualRecipientId(long recipientId
) {
573 while (recipientsMerged
.containsKey(recipientId
)) {
574 final var newRecipientId
= recipientsMerged
.get(recipientId
);
575 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
576 recipientId
= newRecipientId
;
581 private void storeContact(
582 final Connection connection
, final RecipientId recipientId
, final Contact contact
583 ) throws SQLException
{
587 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
590 ).formatted(TABLE_RECIPIENT
);
591 try (final var statement
= connection
.prepareStatement(sql
)) {
592 statement
.setString(1, contact
== null ?
null : contact
.getGivenName());
593 statement
.setString(2, contact
== null ?
null : contact
.getFamilyName());
594 statement
.setInt(3, contact
== null ?
0 : contact
.getMessageExpirationTime());
595 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
596 statement
.setString(5, contact
== null ?
null : contact
.getColor());
597 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
598 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
599 statement
.setLong(8, recipientId
.id());
600 statement
.executeUpdate();
604 private void storeExpiringProfileKeyCredential(
605 final Connection connection
,
606 final RecipientId recipientId
,
607 final ExpiringProfileKeyCredential profileKeyCredential
608 ) throws SQLException
{
612 SET profile_key_credential = ?
615 ).formatted(TABLE_RECIPIENT
);
616 try (final var statement
= connection
.prepareStatement(sql
)) {
617 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
618 statement
.setLong(2, recipientId
.id());
619 statement
.executeUpdate();
623 private void storeProfile(
624 final Connection connection
, final RecipientId recipientId
, final Profile profile
625 ) throws SQLException
{
629 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 = ?
632 ).formatted(TABLE_RECIPIENT
);
633 try (final var statement
= connection
.prepareStatement(sql
)) {
634 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
635 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
636 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
637 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
638 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
639 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
640 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
641 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
642 statement
.setString(9,
645 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
646 statement
.setLong(10, recipientId
.id());
647 statement
.executeUpdate();
651 private void storeProfileKey(
652 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
653 ) throws SQLException
{
654 if (profileKey
!= null) {
655 final var recipientProfileKey
= getProfileKey(recipientId
);
656 if (profileKey
.equals(recipientProfileKey
)) {
657 final var recipientProfile
= getProfile(recipientId
);
658 if (recipientProfile
== null || (
659 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
660 && recipientProfile
.getUnidentifiedAccessMode()
661 != Profile
.UnidentifiedAccessMode
.DISABLED
671 SET profile_key = ?, profile_key_credential = NULL%s
674 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
675 try (final var statement
= connection
.prepareStatement(sql
)) {
676 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
677 statement
.setLong(2, recipientId
.id());
678 statement
.executeUpdate();
682 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
683 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
684 synchronized (recipientsLock
) {
685 try (final var connection
= database
.getConnection()) {
686 connection
.setAutoCommit(false);
687 if (address
.hasSingleIdentifier() || (
688 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
690 pair
= new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
692 pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
694 for (final var toBeMergedRecipientId
: pair
.second()) {
695 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
699 } catch (SQLException e
) {
700 throw new RuntimeException("Failed update recipient store", e
);
704 if (!pair
.second().isEmpty()) {
705 try (final var connection
= database
.getConnection()) {
706 for (final var toBeMergedRecipientId
: pair
.second()) {
707 recipientMergeHandler
.mergeRecipients(connection
, pair
.first(), toBeMergedRecipientId
);
708 deleteRecipient(connection
, toBeMergedRecipientId
);
709 synchronized (recipientsLock
) {
710 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(toBeMergedRecipientId
));
713 } catch (SQLException e
) {
714 throw new RuntimeException("Failed update recipient store", e
);
720 private RecipientId
resolveRecipientLocked(
721 Connection connection
, RecipientAddress address
722 ) throws SQLException
{
723 final var byServiceId
= address
.serviceId().isEmpty()
724 ? Optional
.<RecipientWithAddress
>empty()
725 : findByServiceId(connection
, address
.serviceId().get());
727 if (byServiceId
.isPresent()) {
728 return byServiceId
.get().id();
731 final var byPni
= address
.pni().isEmpty()
732 ? Optional
.<RecipientWithAddress
>empty()
733 : findByServiceId(connection
, address
.pni().get());
735 if (byPni
.isPresent()) {
736 return byPni
.get().id();
739 final var byNumber
= address
.number().isEmpty()
740 ? Optional
.<RecipientWithAddress
>empty()
741 : findByNumber(connection
, address
.number().get());
743 if (byNumber
.isPresent()) {
744 return byNumber
.get().id();
747 logger
.debug("Got new recipient, both serviceId and number are unknown");
749 if (address
.serviceId().isEmpty()) {
750 return addNewRecipient(connection
, address
);
753 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
756 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
757 final var recipient
= findByServiceId(connection
, serviceId
);
759 if (recipient
.isEmpty()) {
760 logger
.debug("Got new recipient, serviceId is unknown");
761 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
764 return recipient
.get().id();
767 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
768 final var recipient
= findByNumber(connection
, number
);
770 if (recipient
.isEmpty()) {
771 logger
.debug("Got new recipient, number is unknown");
772 return addNewRecipient(connection
, new RecipientAddress(null, number
));
775 return recipient
.get().id();
778 private RecipientId
addNewRecipient(
779 final Connection connection
, final RecipientAddress address
780 ) throws SQLException
{
783 INSERT INTO %s (number, uuid, pni)
787 ).formatted(TABLE_RECIPIENT
);
788 try (final var statement
= connection
.prepareStatement(sql
)) {
789 statement
.setString(1, address
.number().orElse(null));
790 statement
.setBytes(2,
791 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
792 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
793 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
794 if (generatedKey
.isPresent()) {
795 final var recipientId
= new RecipientId(generatedKey
.get(), this);
796 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
799 throw new RuntimeException("Failed to add new recipient to database");
804 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
805 synchronized (recipientsLock
) {
806 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
810 SET number = NULL, uuid = NULL, pni = NULL
813 ).formatted(TABLE_RECIPIENT
);
814 try (final var statement
= connection
.prepareStatement(sql
)) {
815 statement
.setLong(1, recipientId
.id());
816 statement
.executeUpdate();
821 private void updateRecipientAddress(
822 Connection connection
, RecipientId recipientId
, final RecipientAddress address
823 ) throws SQLException
{
824 synchronized (recipientsLock
) {
825 recipientAddressCache
.entrySet().removeIf(e
-> e
.getValue().id().equals(recipientId
));
829 SET number = ?, uuid = ?, pni = ?, username = ?
832 ).formatted(TABLE_RECIPIENT
);
833 try (final var statement
= connection
.prepareStatement(sql
)) {
834 statement
.setString(1, address
.number().orElse(null));
835 statement
.setBytes(2,
836 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
837 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
838 statement
.setString(4, address
.username().orElse(null));
839 statement
.setLong(5, recipientId
.id());
840 statement
.executeUpdate();
845 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
851 ).formatted(TABLE_RECIPIENT
);
852 try (final var statement
= connection
.prepareStatement(sql
)) {
853 statement
.setLong(1, recipientId
.id());
854 statement
.executeUpdate();
858 private void mergeRecipientsLocked(
859 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
860 ) throws SQLException
{
861 final var contact
= getContact(connection
, recipientId
);
862 if (contact
== null) {
863 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
864 storeContact(connection
, recipientId
, toBeMergedContact
);
867 final var profileKey
= getProfileKey(connection
, recipientId
);
868 if (profileKey
== null) {
869 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
870 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
873 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
874 if (profileKeyCredential
== null) {
875 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
876 toBeMergedRecipientId
);
877 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
880 final var profile
= getProfile(connection
, recipientId
);
881 if (profile
== null) {
882 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
883 storeProfile(connection
, recipientId
, toBeMergedProfile
);
886 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
889 private Optional
<RecipientWithAddress
> findByNumber(
890 final Connection connection
, final String number
891 ) throws SQLException
{
893 SELECT r._id, r.number, r.uuid, r.pni, r.username
897 """.formatted(TABLE_RECIPIENT
);
898 try (final var statement
= connection
.prepareStatement(sql
)) {
899 statement
.setString(1, number
);
900 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
904 private Optional
<RecipientWithAddress
> findByUsername(
905 final Connection connection
, final String username
906 ) throws SQLException
{
908 SELECT r._id, r.number, r.uuid, r.pni, r.username
912 """.formatted(TABLE_RECIPIENT
);
913 try (final var statement
= connection
.prepareStatement(sql
)) {
914 statement
.setString(1, username
);
915 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
919 private Optional
<RecipientWithAddress
> findByServiceId(
920 final Connection connection
, final ServiceId serviceId
921 ) throws SQLException
{
922 var recipientWithAddress
= Optional
.ofNullable(recipientAddressCache
.get(serviceId
));
923 if (recipientWithAddress
.isPresent()) {
924 return recipientWithAddress
;
927 SELECT r._id, r.number, r.uuid, r.pni, r.username
929 WHERE r.uuid = ?1 OR r.pni = ?1
931 """.formatted(TABLE_RECIPIENT
);
932 try (final var statement
= connection
.prepareStatement(sql
)) {
933 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.getRawUuid()));
934 recipientWithAddress
= Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
935 recipientWithAddress
.ifPresent(r
-> recipientAddressCache
.put(serviceId
, r
));
936 return recipientWithAddress
;
940 private Set
<RecipientWithAddress
> findAllByAddress(
941 final Connection connection
, final RecipientAddress address
942 ) throws SQLException
{
944 SELECT r._id, r.number, r.uuid, r.pni, r.username
946 WHERE r.uuid = ?1 OR r.pni = ?1 OR
947 r.uuid = ?2 OR r.pni = ?2 OR
950 """.formatted(TABLE_RECIPIENT
);
951 try (final var statement
= connection
.prepareStatement(sql
)) {
952 statement
.setBytes(1,
953 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
954 statement
.setBytes(2, address
.pni().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
955 statement
.setString(3, address
.number().orElse(null));
956 statement
.setString(4, address
.username().orElse(null));
957 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
958 .collect(Collectors
.toSet());
962 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
965 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
967 WHERE r._id = ? AND (%s)
969 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
970 try (final var statement
= connection
.prepareStatement(sql
)) {
971 statement
.setLong(1, recipientId
.id());
972 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
976 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
983 ).formatted(TABLE_RECIPIENT
);
984 try (final var statement
= connection
.prepareStatement(sql
)) {
985 statement
.setLong(1, recipientId
.id());
986 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
990 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
991 final Connection connection
, final RecipientId recipientId
992 ) throws SQLException
{
995 SELECT r.profile_key_credential
999 ).formatted(TABLE_RECIPIENT
);
1000 try (final var statement
= connection
.prepareStatement(sql
)) {
1001 statement
.setLong(1, recipientId
.id());
1002 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
1007 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
1010 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
1012 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
1014 ).formatted(TABLE_RECIPIENT
);
1015 try (final var statement
= connection
.prepareStatement(sql
)) {
1016 statement
.setLong(1, recipientId
.id());
1017 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1021 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1022 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(UuidUtil
::parseOrNull
).map(PNI
::from
);
1023 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
1024 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
1025 .equals(pni
.get().getRawUuid()) ? pni
.<ServiceId
>map(p
-> p
) : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
1026 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1027 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1028 return new RecipientAddress(serviceId
, pni
, number
, username
);
1031 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1032 return new RecipientId(resultSet
.getLong("_id"), this);
1035 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1036 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1037 getRecipientAddressFromResultSet(resultSet
));
1040 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1041 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1042 getRecipientAddressFromResultSet(resultSet
),
1043 getContactFromResultSet(resultSet
),
1044 getProfileKeyFromResultSet(resultSet
),
1045 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1046 getProfileFromResultSet(resultSet
));
1049 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1050 return new Contact(resultSet
.getString("given_name"),
1051 resultSet
.getString("family_name"),
1052 resultSet
.getString("color"),
1053 resultSet
.getInt("expiration_time"),
1054 resultSet
.getBoolean("blocked"),
1055 resultSet
.getBoolean("archived"),
1056 resultSet
.getBoolean("profile_sharing"));
1059 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1060 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1061 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1062 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1063 resultSet
.getString("profile_given_name"),
1064 resultSet
.getString("profile_family_name"),
1065 resultSet
.getString("profile_about"),
1066 resultSet
.getString("profile_about_emoji"),
1067 resultSet
.getString("profile_avatar_url_path"),
1068 resultSet
.getBytes("profile_mobile_coin_address"),
1069 profileUnidentifiedAccessMode
== null
1070 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1071 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1072 profileCapabilities
== null
1074 : Arrays
.stream(profileCapabilities
.split(","))
1075 .map(Profile
.Capability
::valueOfOrNull
)
1076 .filter(Objects
::nonNull
)
1077 .collect(Collectors
.toSet()));
1080 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1081 final var profileKey
= resultSet
.getBytes("profile_key");
1083 if (profileKey
== null) {
1087 return new ProfileKey(profileKey
);
1088 } catch (InvalidInputException ignored
) {
1093 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1094 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1096 if (profileKeyCredential
== null) {
1100 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1101 } catch (Throwable ignored
) {
1106 public interface RecipientMergeHandler
{
1108 void mergeRecipients(
1109 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1110 ) throws SQLException
;
1113 private class HelperStore
implements MergeRecipientHelper
.Store
{
1115 private final Connection connection
;
1117 public HelperStore(final Connection connection
) {
1118 this.connection
= connection
;
1122 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1123 return RecipientStore
.this.findAllByAddress(connection
, address
);
1127 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1128 return RecipientStore
.this.addNewRecipient(connection
, address
);
1132 public void updateRecipientAddress(
1133 final RecipientId recipientId
, final RecipientAddress address
1134 ) throws SQLException
{
1135 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1139 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1140 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);