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 public static void createSql(Connection connection
) throws SQLException
{
51 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
52 try (final var statement
= connection
.createStatement()) {
53 statement
.executeUpdate("""
54 CREATE TABLE recipient (
55 _id INTEGER PRIMARY KEY AUTOINCREMENT,
61 profile_key_credential BLOB,
67 expiration_time INTEGER NOT NULL DEFAULT 0,
68 blocked INTEGER NOT NULL DEFAULT FALSE,
69 archived INTEGER NOT NULL DEFAULT FALSE,
70 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
72 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
73 profile_given_name TEXT,
74 profile_family_name TEXT,
76 profile_about_emoji TEXT,
77 profile_avatar_url_path TEXT,
78 profile_mobile_coin_address BLOB,
79 profile_unidentified_access_mode TEXT,
80 profile_capabilities TEXT
86 public RecipientStore(
87 final RecipientMergeHandler recipientMergeHandler
,
88 final SelfAddressProvider selfAddressProvider
,
89 final Database database
91 this.recipientMergeHandler
= recipientMergeHandler
;
92 this.selfAddressProvider
= selfAddressProvider
;
93 this.database
= database
;
96 public RecipientAddress
resolveRecipientAddress(RecipientId recipientId
) {
99 SELECT r.number, r.uuid, r.pni, r.username
103 ).formatted(TABLE_RECIPIENT
);
104 try (final var connection
= database
.getConnection()) {
105 try (final var statement
= connection
.prepareStatement(sql
)) {
106 statement
.setLong(1, recipientId
.id());
107 return Utils
.executeQuerySingleRow(statement
, this::getRecipientAddressFromResultSet
);
109 } catch (SQLException e
) {
110 throw new RuntimeException("Failed read from recipient store", e
);
114 public Collection
<RecipientId
> getRecipientIdsWithEnabledProfileSharing() {
119 WHERE r.blocked = FALSE AND r.profile_sharing = TRUE
121 ).formatted(TABLE_RECIPIENT
);
122 try (final var connection
= database
.getConnection()) {
123 try (final var statement
= connection
.prepareStatement(sql
)) {
124 try (var result
= Utils
.executeQueryForStream(statement
, this::getRecipientIdFromResultSet
)) {
125 return result
.toList();
128 } catch (SQLException e
) {
129 throw new RuntimeException("Failed read from recipient store", e
);
134 public RecipientId
resolveRecipient(final long rawRecipientId
) {
141 ).formatted(TABLE_RECIPIENT
);
142 try (final var connection
= database
.getConnection()) {
143 try (final var statement
= connection
.prepareStatement(sql
)) {
144 statement
.setLong(1, rawRecipientId
);
145 return Utils
.executeQueryForOptional(statement
, this::getRecipientIdFromResultSet
).orElse(null);
147 } catch (SQLException e
) {
148 throw new RuntimeException("Failed read from recipient store", e
);
153 public RecipientId
resolveRecipient(final String identifier
) {
154 final var serviceId
= ServiceId
.parseOrNull(identifier
);
155 if (serviceId
!= null) {
156 return resolveRecipient(serviceId
);
158 return resolveRecipientByNumber(identifier
);
162 private RecipientId
resolveRecipientByNumber(final String number
) {
163 synchronized (recipientsLock
) {
164 final RecipientId recipientId
;
165 try (final var connection
= database
.getConnection()) {
166 connection
.setAutoCommit(false);
167 recipientId
= resolveRecipientLocked(connection
, number
);
169 } catch (SQLException e
) {
170 throw new RuntimeException("Failed read recipient store", e
);
177 public RecipientId
resolveRecipient(final ServiceId serviceId
) {
178 synchronized (recipientsLock
) {
179 final RecipientId recipientId
;
180 try (final var connection
= database
.getConnection()) {
181 connection
.setAutoCommit(false);
182 recipientId
= resolveRecipientLocked(connection
, serviceId
);
184 } catch (SQLException e
) {
185 throw new RuntimeException("Failed read recipient store", e
);
192 * Should only be used for recipientIds from the database.
193 * Where the foreign key relations ensure a valid recipientId.
196 public RecipientId
create(final long recipientId
) {
197 return new RecipientId(recipientId
, this);
200 public RecipientId
resolveRecipientByNumber(
201 final String number
, Supplier
<ServiceId
> serviceIdSupplier
202 ) throws UnregisteredRecipientException
{
203 final Optional
<RecipientWithAddress
> byNumber
;
204 try (final var connection
= database
.getConnection()) {
205 byNumber
= findByNumber(connection
, number
);
206 } catch (SQLException e
) {
207 throw new RuntimeException("Failed read from recipient store", e
);
209 if (byNumber
.isEmpty() || byNumber
.get().address().serviceId().isEmpty()) {
210 final var serviceId
= serviceIdSupplier
.get();
211 if (serviceId
== null) {
212 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
216 return resolveRecipient(serviceId
);
218 return byNumber
.get().id();
221 public Optional
<RecipientId
> resolveRecipientByNumberOptional(final String number
) {
222 final Optional
<RecipientWithAddress
> byNumber
;
223 try (final var connection
= database
.getConnection()) {
224 byNumber
= findByNumber(connection
, number
);
225 } catch (SQLException e
) {
226 throw new RuntimeException("Failed read from recipient store", e
);
228 return byNumber
.map(RecipientWithAddress
::id
);
231 public RecipientId
resolveRecipientByUsername(
232 final String username
, Supplier
<ServiceId
> serviceIdSupplier
233 ) throws UnregisteredRecipientException
{
234 final Optional
<RecipientWithAddress
> byUsername
;
235 try (final var connection
= database
.getConnection()) {
236 byUsername
= findByUsername(connection
, username
);
237 } catch (SQLException e
) {
238 throw new RuntimeException("Failed read from recipient store", e
);
240 if (byUsername
.isEmpty() || byUsername
.get().address().serviceId().isEmpty()) {
241 final var serviceId
= serviceIdSupplier
.get();
242 if (serviceId
== null) {
243 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
248 return resolveRecipient(serviceId
);
250 return byUsername
.get().id();
253 public RecipientId
resolveRecipient(RecipientAddress address
) {
254 synchronized (recipientsLock
) {
255 final RecipientId recipientId
;
256 try (final var connection
= database
.getConnection()) {
257 connection
.setAutoCommit(false);
258 recipientId
= resolveRecipientLocked(connection
, address
);
260 } catch (SQLException e
) {
261 throw new RuntimeException("Failed read recipient store", e
);
268 public RecipientId
resolveSelfRecipientTrusted(RecipientAddress address
) {
269 return resolveRecipientTrusted(address
, true);
272 public RecipientId
resolveRecipientTrusted(RecipientAddress address
) {
273 return resolveRecipientTrusted(address
, false);
277 public RecipientId
resolveRecipientTrusted(SignalServiceAddress address
) {
278 return resolveRecipientTrusted(new RecipientAddress(address
), false);
282 public RecipientId
resolveRecipientTrusted(
283 final Optional
<ACI
> aci
, final Optional
<PNI
> pni
, final Optional
<String
> number
285 final var serviceId
= aci
.map(a
-> (ServiceId
) a
).or(() -> pni
);
286 return resolveRecipientTrusted(new RecipientAddress(serviceId
, pni
, number
, Optional
.empty()), false);
290 public RecipientId
resolveRecipientTrusted(final ServiceId serviceId
, final String username
) {
291 return resolveRecipientTrusted(new RecipientAddress(serviceId
, null, null, username
), false);
294 public RecipientId
resolveRecipientTrusted(
295 final ACI aci
, final String username
297 return resolveRecipientTrusted(new RecipientAddress(Optional
.of(aci
),
300 Optional
.of(username
)), false);
304 public void storeContact(RecipientId recipientId
, final Contact contact
) {
305 try (final var connection
= database
.getConnection()) {
306 storeContact(connection
, recipientId
, contact
);
307 } catch (SQLException e
) {
308 throw new RuntimeException("Failed update recipient store", e
);
313 public Contact
getContact(RecipientId recipientId
) {
314 try (final var connection
= database
.getConnection()) {
315 return getContact(connection
, recipientId
);
316 } catch (SQLException e
) {
317 throw new RuntimeException("Failed read from recipient store", e
);
322 public List
<Pair
<RecipientId
, Contact
>> getContacts() {
325 SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
327 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
329 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
330 try (final var connection
= database
.getConnection()) {
331 try (final var statement
= connection
.prepareStatement(sql
)) {
332 try (var result
= Utils
.executeQueryForStream(statement
,
333 resultSet
-> new Pair
<>(getRecipientIdFromResultSet(resultSet
),
334 getContactFromResultSet(resultSet
)))) {
335 return result
.toList();
338 } catch (SQLException e
) {
339 throw new RuntimeException("Failed read from recipient store", e
);
343 public List
<Recipient
> getRecipients(
344 boolean onlyContacts
, Optional
<Boolean
> blocked
, Set
<RecipientId
> recipientIds
, Optional
<String
> name
346 final var sqlWhere
= new ArrayList
<String
>();
348 sqlWhere
.add("(" + SQL_IS_CONTACT
+ ")");
350 if (blocked
.isPresent()) {
351 sqlWhere
.add("r.blocked = ?");
353 if (!recipientIds
.isEmpty()) {
354 final var recipientIdsCommaSeparated
= recipientIds
.stream()
355 .map(recipientId
-> String
.valueOf(recipientId
.id()))
356 .collect(Collectors
.joining(","));
357 sqlWhere
.add("r._id IN (" + recipientIdsCommaSeparated
+ ")");
362 r.number, r.uuid, r.pni, r.username,
363 r.profile_key, r.profile_key_credential,
364 r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
365 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
367 WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s
369 ).formatted(TABLE_RECIPIENT
, sqlWhere
.size() == 0 ?
"TRUE" : String
.join(" AND ", sqlWhere
));
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()))).toList();
381 } catch (SQLException e
) {
382 throw new RuntimeException("Failed read from recipient store", e
);
386 public Set
<String
> getAllNumbers() {
391 WHERE r.number IS NOT NULL
393 ).formatted(TABLE_RECIPIENT
);
394 try (final var connection
= database
.getConnection()) {
395 try (final var statement
= connection
.prepareStatement(sql
)) {
396 return Utils
.executeQueryForStream(statement
, resultSet
-> resultSet
.getString("number"))
397 .filter(Objects
::nonNull
)
402 } catch (NumberFormatException e
) {
406 .collect(Collectors
.toSet());
408 } catch (SQLException e
) {
409 throw new RuntimeException("Failed read from recipient store", e
);
413 public Map
<ServiceId
, ProfileKey
> getServiceIdToProfileKeyMap() {
416 SELECT r.uuid, r.profile_key
418 WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL
420 ).formatted(TABLE_RECIPIENT
);
421 try (final var connection
= database
.getConnection()) {
422 try (final var statement
= connection
.prepareStatement(sql
)) {
423 return Utils
.executeQueryForStream(statement
, resultSet
-> {
424 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
425 final var profileKey
= getProfileKeyFromResultSet(resultSet
);
426 return new Pair
<>(serviceId
, profileKey
);
427 }).filter(Objects
::nonNull
).collect(Collectors
.toMap(Pair
::first
, Pair
::second
));
429 } catch (SQLException e
) {
430 throw new RuntimeException("Failed read from recipient store", e
);
435 public void deleteContact(RecipientId recipientId
) {
436 storeContact(recipientId
, null);
439 public void deleteRecipientData(RecipientId recipientId
) {
440 logger
.debug("Deleting recipient data for {}", recipientId
);
441 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
);
455 public Profile
getProfile(final RecipientId recipientId
) {
456 try (final var connection
= database
.getConnection()) {
457 return getProfile(connection
, recipientId
);
458 } catch (SQLException e
) {
459 throw new RuntimeException("Failed read from recipient store", e
);
464 public ProfileKey
getProfileKey(final RecipientId recipientId
) {
465 try (final var connection
= database
.getConnection()) {
466 return getProfileKey(connection
, recipientId
);
467 } catch (SQLException e
) {
468 throw new RuntimeException("Failed read from recipient store", e
);
473 public ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(final RecipientId recipientId
) {
474 try (final var connection
= database
.getConnection()) {
475 return getExpiringProfileKeyCredential(connection
, recipientId
);
476 } catch (SQLException e
) {
477 throw new RuntimeException("Failed read from recipient store", e
);
482 public void storeProfile(RecipientId recipientId
, final Profile profile
) {
483 try (final var connection
= database
.getConnection()) {
484 storeProfile(connection
, recipientId
, profile
);
485 } catch (SQLException e
) {
486 throw new RuntimeException("Failed update recipient store", e
);
491 public void storeSelfProfileKey(final RecipientId recipientId
, final ProfileKey profileKey
) {
492 try (final var connection
= database
.getConnection()) {
493 storeProfileKey(connection
, recipientId
, profileKey
, false);
494 } catch (SQLException e
) {
495 throw new RuntimeException("Failed update recipient store", e
);
500 public void storeProfileKey(RecipientId recipientId
, final ProfileKey profileKey
) {
501 try (final var connection
= database
.getConnection()) {
502 storeProfileKey(connection
, recipientId
, profileKey
, true);
503 } catch (SQLException e
) {
504 throw new RuntimeException("Failed update recipient store", e
);
509 public void storeExpiringProfileKeyCredential(
510 RecipientId recipientId
, final ExpiringProfileKeyCredential profileKeyCredential
512 try (final var connection
= database
.getConnection()) {
513 storeExpiringProfileKeyCredential(connection
, recipientId
, profileKeyCredential
);
514 } catch (SQLException e
) {
515 throw new RuntimeException("Failed update recipient store", e
);
519 void addLegacyRecipients(final Map
<RecipientId
, Recipient
> recipients
) {
520 logger
.debug("Migrating legacy recipients to database");
521 long start
= System
.nanoTime();
524 INSERT INTO %s (_id, number, uuid)
527 ).formatted(TABLE_RECIPIENT
);
528 try (final var connection
= database
.getConnection()) {
529 connection
.setAutoCommit(false);
530 try (final var statement
= connection
.prepareStatement("DELETE FROM %s".formatted(TABLE_RECIPIENT
))) {
531 statement
.executeUpdate();
533 try (final var statement
= connection
.prepareStatement(sql
)) {
534 for (final var recipient
: recipients
.values()) {
535 statement
.setLong(1, recipient
.getRecipientId().id());
536 statement
.setString(2, recipient
.getAddress().number().orElse(null));
537 statement
.setBytes(3,
538 recipient
.getAddress()
540 .map(ServiceId
::getRawUuid
)
541 .map(UuidUtil
::toByteArray
)
543 statement
.executeUpdate();
546 logger
.debug("Initial inserts took {}ms", (System
.nanoTime() - start
) / 1000000);
548 for (final var recipient
: recipients
.values()) {
549 if (recipient
.getContact() != null) {
550 storeContact(connection
, recipient
.getRecipientId(), recipient
.getContact());
552 if (recipient
.getProfile() != null) {
553 storeProfile(connection
, recipient
.getRecipientId(), recipient
.getProfile());
555 if (recipient
.getProfileKey() != null) {
556 storeProfileKey(connection
, recipient
.getRecipientId(), recipient
.getProfileKey(), false);
558 if (recipient
.getExpiringProfileKeyCredential() != null) {
559 storeExpiringProfileKeyCredential(connection
,
560 recipient
.getRecipientId(),
561 recipient
.getExpiringProfileKeyCredential());
565 } catch (SQLException e
) {
566 throw new RuntimeException("Failed update recipient store", e
);
568 logger
.debug("Complete recipients migration took {}ms", (System
.nanoTime() - start
) / 1000000);
571 long getActualRecipientId(long recipientId
) {
572 while (recipientsMerged
.containsKey(recipientId
)) {
573 final var newRecipientId
= recipientsMerged
.get(recipientId
);
574 logger
.debug("Using {} instead of {}, because recipients have been merged", newRecipientId
, recipientId
);
575 recipientId
= newRecipientId
;
580 private void storeContact(
581 final Connection connection
, final RecipientId recipientId
, final Contact contact
582 ) throws SQLException
{
586 SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?
589 ).formatted(TABLE_RECIPIENT
);
590 try (final var statement
= connection
.prepareStatement(sql
)) {
591 statement
.setString(1, contact
== null ?
null : contact
.getGivenName());
592 statement
.setString(2, contact
== null ?
null : contact
.getFamilyName());
593 statement
.setInt(3, contact
== null ?
0 : contact
.getMessageExpirationTime());
594 statement
.setBoolean(4, contact
!= null && contact
.isProfileSharingEnabled());
595 statement
.setString(5, contact
== null ?
null : contact
.getColor());
596 statement
.setBoolean(6, contact
!= null && contact
.isBlocked());
597 statement
.setBoolean(7, contact
!= null && contact
.isArchived());
598 statement
.setLong(8, recipientId
.id());
599 statement
.executeUpdate();
603 private void storeExpiringProfileKeyCredential(
604 final Connection connection
,
605 final RecipientId recipientId
,
606 final ExpiringProfileKeyCredential profileKeyCredential
607 ) throws SQLException
{
611 SET profile_key_credential = ?
614 ).formatted(TABLE_RECIPIENT
);
615 try (final var statement
= connection
.prepareStatement(sql
)) {
616 statement
.setBytes(1, profileKeyCredential
== null ?
null : profileKeyCredential
.serialize());
617 statement
.setLong(2, recipientId
.id());
618 statement
.executeUpdate();
622 private void storeProfile(
623 final Connection connection
, final RecipientId recipientId
, final Profile profile
624 ) throws SQLException
{
628 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 = ?
631 ).formatted(TABLE_RECIPIENT
);
632 try (final var statement
= connection
.prepareStatement(sql
)) {
633 statement
.setLong(1, profile
== null ?
0 : profile
.getLastUpdateTimestamp());
634 statement
.setString(2, profile
== null ?
null : profile
.getGivenName());
635 statement
.setString(3, profile
== null ?
null : profile
.getFamilyName());
636 statement
.setString(4, profile
== null ?
null : profile
.getAbout());
637 statement
.setString(5, profile
== null ?
null : profile
.getAboutEmoji());
638 statement
.setString(6, profile
== null ?
null : profile
.getAvatarUrlPath());
639 statement
.setBytes(7, profile
== null ?
null : profile
.getMobileCoinAddress());
640 statement
.setString(8, profile
== null ?
null : profile
.getUnidentifiedAccessMode().name());
641 statement
.setString(9,
644 : profile
.getCapabilities().stream().map(Enum
::name
).collect(Collectors
.joining(",")));
645 statement
.setLong(10, recipientId
.id());
646 statement
.executeUpdate();
650 private void storeProfileKey(
651 Connection connection
, RecipientId recipientId
, final ProfileKey profileKey
, boolean resetProfile
652 ) throws SQLException
{
653 if (profileKey
!= null) {
654 final var recipientProfileKey
= getProfileKey(recipientId
);
655 if (profileKey
.equals(recipientProfileKey
)) {
656 final var recipientProfile
= getProfile(recipientId
);
657 if (recipientProfile
== null || (
658 recipientProfile
.getUnidentifiedAccessMode() != Profile
.UnidentifiedAccessMode
.UNKNOWN
659 && recipientProfile
.getUnidentifiedAccessMode()
660 != Profile
.UnidentifiedAccessMode
.DISABLED
670 SET profile_key = ?, profile_key_credential = NULL%s
673 ).formatted(TABLE_RECIPIENT
, resetProfile ?
", profile_last_update_timestamp = 0" : "");
674 try (final var statement
= connection
.prepareStatement(sql
)) {
675 statement
.setBytes(1, profileKey
== null ?
null : profileKey
.serialize());
676 statement
.setLong(2, recipientId
.id());
677 statement
.executeUpdate();
681 private RecipientId
resolveRecipientTrusted(RecipientAddress address
, boolean isSelf
) {
682 final Pair
<RecipientId
, List
<RecipientId
>> pair
;
683 synchronized (recipientsLock
) {
684 try (final var connection
= database
.getConnection()) {
685 connection
.setAutoCommit(false);
686 if (address
.hasSingleIdentifier() || (
687 !isSelf
&& selfAddressProvider
.getSelfAddress().matches(address
)
689 pair
= new Pair
<>(resolveRecipientLocked(connection
, address
), List
.of());
691 pair
= MergeRecipientHelper
.resolveRecipientTrustedLocked(new HelperStore(connection
), address
);
693 for (final var toBeMergedRecipientId
: pair
.second()) {
694 mergeRecipientsLocked(connection
, pair
.first(), toBeMergedRecipientId
);
698 } catch (SQLException e
) {
699 throw new RuntimeException("Failed update recipient store", e
);
703 if (pair
.second().size() > 0) {
704 try (final var connection
= database
.getConnection()) {
705 for (final var toBeMergedRecipientId
: pair
.second()) {
706 recipientMergeHandler
.mergeRecipients(connection
, pair
.first(), toBeMergedRecipientId
);
707 deleteRecipient(connection
, toBeMergedRecipientId
);
709 } catch (SQLException e
) {
710 throw new RuntimeException("Failed update recipient store", e
);
716 private RecipientId
resolveRecipientLocked(
717 Connection connection
, RecipientAddress address
718 ) throws SQLException
{
719 final var byServiceId
= address
.serviceId().isEmpty()
720 ? Optional
.<RecipientWithAddress
>empty()
721 : findByServiceId(connection
, address
.serviceId().get());
723 if (byServiceId
.isPresent()) {
724 return byServiceId
.get().id();
727 final var byPni
= address
.pni().isEmpty()
728 ? Optional
.<RecipientWithAddress
>empty()
729 : findByServiceId(connection
, address
.pni().get());
731 if (byPni
.isPresent()) {
732 return byPni
.get().id();
735 final var byNumber
= address
.number().isEmpty()
736 ? Optional
.<RecipientWithAddress
>empty()
737 : findByNumber(connection
, address
.number().get());
739 if (byNumber
.isPresent()) {
740 return byNumber
.get().id();
743 logger
.debug("Got new recipient, both serviceId and number are unknown");
745 if (address
.serviceId().isEmpty()) {
746 return addNewRecipient(connection
, address
);
749 return addNewRecipient(connection
, new RecipientAddress(address
.serviceId().get()));
752 private RecipientId
resolveRecipientLocked(Connection connection
, ServiceId serviceId
) throws SQLException
{
753 final var recipient
= findByServiceId(connection
, serviceId
);
755 if (recipient
.isEmpty()) {
756 logger
.debug("Got new recipient, serviceId is unknown");
757 return addNewRecipient(connection
, new RecipientAddress(serviceId
));
760 return recipient
.get().id();
763 private RecipientId
resolveRecipientLocked(Connection connection
, String number
) throws SQLException
{
764 final var recipient
= findByNumber(connection
, number
);
766 if (recipient
.isEmpty()) {
767 logger
.debug("Got new recipient, number is unknown");
768 return addNewRecipient(connection
, new RecipientAddress(null, number
));
771 return recipient
.get().id();
774 private RecipientId
addNewRecipient(
775 final Connection connection
, final RecipientAddress address
776 ) throws SQLException
{
779 INSERT INTO %s (number, uuid, pni)
783 ).formatted(TABLE_RECIPIENT
);
784 try (final var statement
= connection
.prepareStatement(sql
)) {
785 statement
.setString(1, address
.number().orElse(null));
786 statement
.setBytes(2,
787 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
788 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
789 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
790 if (generatedKey
.isPresent()) {
791 final var recipientId
= new RecipientId(generatedKey
.get(), this);
792 logger
.debug("Added new recipient {} with address {}", recipientId
, address
);
795 throw new RuntimeException("Failed to add new recipient to database");
800 private void removeRecipientAddress(Connection connection
, RecipientId recipientId
) throws SQLException
{
804 SET number = NULL, uuid = NULL, pni = NULL
807 ).formatted(TABLE_RECIPIENT
);
808 try (final var statement
= connection
.prepareStatement(sql
)) {
809 statement
.setLong(1, recipientId
.id());
810 statement
.executeUpdate();
814 private void updateRecipientAddress(
815 Connection connection
, RecipientId recipientId
, final RecipientAddress address
816 ) throws SQLException
{
820 SET number = ?, uuid = ?, pni = ?, username = ?
823 ).formatted(TABLE_RECIPIENT
);
824 try (final var statement
= connection
.prepareStatement(sql
)) {
825 statement
.setString(1, address
.number().orElse(null));
826 statement
.setBytes(2,
827 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
828 statement
.setBytes(3, address
.pni().map(PNI
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
829 statement
.setString(4, address
.username().orElse(null));
830 statement
.setLong(5, recipientId
.id());
831 statement
.executeUpdate();
835 private void deleteRecipient(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
841 ).formatted(TABLE_RECIPIENT
);
842 try (final var statement
= connection
.prepareStatement(sql
)) {
843 statement
.setLong(1, recipientId
.id());
844 statement
.executeUpdate();
848 private void mergeRecipientsLocked(
849 Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
850 ) throws SQLException
{
851 final var contact
= getContact(connection
, recipientId
);
852 if (contact
== null) {
853 final var toBeMergedContact
= getContact(connection
, toBeMergedRecipientId
);
854 storeContact(connection
, recipientId
, toBeMergedContact
);
857 final var profileKey
= getProfileKey(connection
, recipientId
);
858 if (profileKey
== null) {
859 final var toBeMergedProfileKey
= getProfileKey(connection
, toBeMergedRecipientId
);
860 storeProfileKey(connection
, recipientId
, toBeMergedProfileKey
, false);
863 final var profileKeyCredential
= getExpiringProfileKeyCredential(connection
, recipientId
);
864 if (profileKeyCredential
== null) {
865 final var toBeMergedProfileKeyCredential
= getExpiringProfileKeyCredential(connection
,
866 toBeMergedRecipientId
);
867 storeExpiringProfileKeyCredential(connection
, recipientId
, toBeMergedProfileKeyCredential
);
870 final var profile
= getProfile(connection
, recipientId
);
871 if (profile
== null) {
872 final var toBeMergedProfile
= getProfile(connection
, toBeMergedRecipientId
);
873 storeProfile(connection
, recipientId
, toBeMergedProfile
);
876 recipientsMerged
.put(toBeMergedRecipientId
.id(), recipientId
.id());
879 private Optional
<RecipientWithAddress
> findByNumber(
880 final Connection connection
, final String number
881 ) throws SQLException
{
883 SELECT r._id, r.number, r.uuid, r.pni, r.username
887 """.formatted(TABLE_RECIPIENT
);
888 try (final var statement
= connection
.prepareStatement(sql
)) {
889 statement
.setString(1, number
);
890 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
894 private Optional
<RecipientWithAddress
> findByUsername(
895 final Connection connection
, final String username
896 ) throws SQLException
{
898 SELECT r._id, r.number, r.uuid, r.pni, r.username
902 """.formatted(TABLE_RECIPIENT
);
903 try (final var statement
= connection
.prepareStatement(sql
)) {
904 statement
.setString(1, username
);
905 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
909 private Optional
<RecipientWithAddress
> findByServiceId(
910 final Connection connection
, final ServiceId serviceId
911 ) throws SQLException
{
913 SELECT r._id, r.number, r.uuid, r.pni, r.username
915 WHERE r.uuid = ?1 OR r.pni = ?1
917 """.formatted(TABLE_RECIPIENT
);
918 try (final var statement
= connection
.prepareStatement(sql
)) {
919 statement
.setBytes(1, UuidUtil
.toByteArray(serviceId
.getRawUuid()));
920 return Utils
.executeQueryForOptional(statement
, this::getRecipientWithAddressFromResultSet
);
924 private Set
<RecipientWithAddress
> findAllByAddress(
925 final Connection connection
, final RecipientAddress address
926 ) throws SQLException
{
928 SELECT r._id, r.number, r.uuid, r.pni, r.username
930 WHERE r.uuid = ?1 OR r.pni = ?1 OR
931 r.uuid = ?2 OR r.pni = ?2 OR
934 """.formatted(TABLE_RECIPIENT
);
935 try (final var statement
= connection
.prepareStatement(sql
)) {
936 statement
.setBytes(1,
937 address
.serviceId().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
938 statement
.setBytes(2, address
.pni().map(ServiceId
::getRawUuid
).map(UuidUtil
::toByteArray
).orElse(null));
939 statement
.setString(3, address
.number().orElse(null));
940 statement
.setString(4, address
.username().orElse(null));
941 return Utils
.executeQueryForStream(statement
, this::getRecipientWithAddressFromResultSet
)
942 .collect(Collectors
.toSet());
946 private Contact
getContact(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
949 SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
951 WHERE r._id = ? AND (%s)
953 ).formatted(TABLE_RECIPIENT
, SQL_IS_CONTACT
);
954 try (final var statement
= connection
.prepareStatement(sql
)) {
955 statement
.setLong(1, recipientId
.id());
956 return Utils
.executeQueryForOptional(statement
, this::getContactFromResultSet
).orElse(null);
960 private ProfileKey
getProfileKey(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
967 ).formatted(TABLE_RECIPIENT
);
968 try (final var statement
= connection
.prepareStatement(sql
)) {
969 statement
.setLong(1, recipientId
.id());
970 return Utils
.executeQueryForOptional(statement
, this::getProfileKeyFromResultSet
).orElse(null);
974 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredential(
975 final Connection connection
, final RecipientId recipientId
976 ) throws SQLException
{
979 SELECT r.profile_key_credential
983 ).formatted(TABLE_RECIPIENT
);
984 try (final var statement
= connection
.prepareStatement(sql
)) {
985 statement
.setLong(1, recipientId
.id());
986 return Utils
.executeQueryForOptional(statement
, this::getExpiringProfileKeyCredentialFromResultSet
)
991 private Profile
getProfile(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
994 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
996 WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
998 ).formatted(TABLE_RECIPIENT
);
999 try (final var statement
= connection
.prepareStatement(sql
)) {
1000 statement
.setLong(1, recipientId
.id());
1001 return Utils
.executeQueryForOptional(statement
, this::getProfileFromResultSet
).orElse(null);
1005 private RecipientAddress
getRecipientAddressFromResultSet(ResultSet resultSet
) throws SQLException
{
1006 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni")).map(UuidUtil
::parseOrNull
).map(PNI
::from
);
1007 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid")).map(UuidUtil
::parseOrNull
);
1008 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
1009 .equals(pni
.get().getRawUuid()) ? pni
.<ServiceId
>map(p
-> p
) : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
1010 final var number
= Optional
.ofNullable(resultSet
.getString("number"));
1011 final var username
= Optional
.ofNullable(resultSet
.getString("username"));
1012 return new RecipientAddress(serviceId
, pni
, number
, username
);
1015 private RecipientId
getRecipientIdFromResultSet(ResultSet resultSet
) throws SQLException
{
1016 return new RecipientId(resultSet
.getLong("_id"), this);
1019 private RecipientWithAddress
getRecipientWithAddressFromResultSet(final ResultSet resultSet
) throws SQLException
{
1020 return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet
),
1021 getRecipientAddressFromResultSet(resultSet
));
1024 private Recipient
getRecipientFromResultSet(final ResultSet resultSet
) throws SQLException
{
1025 return new Recipient(getRecipientIdFromResultSet(resultSet
),
1026 getRecipientAddressFromResultSet(resultSet
),
1027 getContactFromResultSet(resultSet
),
1028 getProfileKeyFromResultSet(resultSet
),
1029 getExpiringProfileKeyCredentialFromResultSet(resultSet
),
1030 getProfileFromResultSet(resultSet
));
1033 private Contact
getContactFromResultSet(ResultSet resultSet
) throws SQLException
{
1034 return new Contact(resultSet
.getString("given_name"),
1035 resultSet
.getString("family_name"),
1036 resultSet
.getString("color"),
1037 resultSet
.getInt("expiration_time"),
1038 resultSet
.getBoolean("blocked"),
1039 resultSet
.getBoolean("archived"),
1040 resultSet
.getBoolean("profile_sharing"));
1043 private Profile
getProfileFromResultSet(ResultSet resultSet
) throws SQLException
{
1044 final var profileCapabilities
= resultSet
.getString("profile_capabilities");
1045 final var profileUnidentifiedAccessMode
= resultSet
.getString("profile_unidentified_access_mode");
1046 return new Profile(resultSet
.getLong("profile_last_update_timestamp"),
1047 resultSet
.getString("profile_given_name"),
1048 resultSet
.getString("profile_family_name"),
1049 resultSet
.getString("profile_about"),
1050 resultSet
.getString("profile_about_emoji"),
1051 resultSet
.getString("profile_avatar_url_path"),
1052 resultSet
.getBytes("profile_mobile_coin_address"),
1053 profileUnidentifiedAccessMode
== null
1054 ? Profile
.UnidentifiedAccessMode
.UNKNOWN
1055 : Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(profileUnidentifiedAccessMode
),
1056 profileCapabilities
== null
1058 : Arrays
.stream(profileCapabilities
.split(","))
1059 .map(Profile
.Capability
::valueOfOrNull
)
1060 .filter(Objects
::nonNull
)
1061 .collect(Collectors
.toSet()));
1064 private ProfileKey
getProfileKeyFromResultSet(ResultSet resultSet
) throws SQLException
{
1065 final var profileKey
= resultSet
.getBytes("profile_key");
1067 if (profileKey
== null) {
1071 return new ProfileKey(profileKey
);
1072 } catch (InvalidInputException ignored
) {
1077 private ExpiringProfileKeyCredential
getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet
) throws SQLException
{
1078 final var profileKeyCredential
= resultSet
.getBytes("profile_key_credential");
1080 if (profileKeyCredential
== null) {
1084 return new ExpiringProfileKeyCredential(profileKeyCredential
);
1085 } catch (Throwable ignored
) {
1090 public interface RecipientMergeHandler
{
1092 void mergeRecipients(
1093 final Connection connection
, RecipientId recipientId
, RecipientId toBeMergedRecipientId
1094 ) throws SQLException
;
1097 private class HelperStore
implements MergeRecipientHelper
.Store
{
1099 private final Connection connection
;
1101 public HelperStore(final Connection connection
) {
1102 this.connection
= connection
;
1106 public Set
<RecipientWithAddress
> findAllByAddress(final RecipientAddress address
) throws SQLException
{
1107 return RecipientStore
.this.findAllByAddress(connection
, address
);
1111 public RecipientId
addNewRecipient(final RecipientAddress address
) throws SQLException
{
1112 return RecipientStore
.this.addNewRecipient(connection
, address
);
1116 public void updateRecipientAddress(
1117 final RecipientId recipientId
, final RecipientAddress address
1118 ) throws SQLException
{
1119 RecipientStore
.this.updateRecipientAddress(connection
, recipientId
, address
);
1123 public void removeRecipientAddress(final RecipientId recipientId
) throws SQLException
{
1124 RecipientStore
.this.removeRecipientAddress(connection
, recipientId
);