1 package org
.asamk
.signal
.manager
.syncStorage
;
3 import org
.asamk
.signal
.manager
.api
.Profile
;
4 import org
.asamk
.signal
.manager
.internal
.JobExecutor
;
5 import org
.asamk
.signal
.manager
.jobs
.CheckWhoAmIJob
;
6 import org
.asamk
.signal
.manager
.jobs
.DownloadProfileAvatarJob
;
7 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
8 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
9 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
10 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
11 import org
.slf4j
.Logger
;
12 import org
.slf4j
.LoggerFactory
;
13 import org
.whispersystems
.signalservice
.api
.push
.UsernameLinkComponents
;
14 import org
.whispersystems
.signalservice
.api
.storage
.IAPSubscriptionId
;
15 import org
.whispersystems
.signalservice
.api
.storage
.SignalAccountRecord
;
16 import org
.whispersystems
.signalservice
.api
.storage
.StorageId
;
17 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
18 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.AccountRecord
;
19 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.OptionalBool
;
21 import java
.sql
.Connection
;
22 import java
.sql
.SQLException
;
23 import java
.util
.Arrays
;
24 import java
.util
.Optional
;
26 import okio
.ByteString
;
28 import static org
.asamk
.signal
.manager
.util
.Utils
.firstNonEmpty
;
29 import static org
.whispersystems
.signalservice
.api
.storage
.AccountRecordExtensionsKt
.safeSetBackupsSubscriber
;
30 import static org
.whispersystems
.signalservice
.api
.storage
.AccountRecordExtensionsKt
.safeSetPayments
;
31 import static org
.whispersystems
.signalservice
.api
.storage
.AccountRecordExtensionsKt
.safeSetSubscriber
;
34 * Processes {@link SignalAccountRecord}s.
36 public class AccountRecordProcessor
extends DefaultStorageRecordProcessor
<SignalAccountRecord
> {
38 private static final Logger logger
= LoggerFactory
.getLogger(AccountRecordProcessor
.class);
39 private final SignalAccountRecord localAccountRecord
;
40 private final SignalAccount account
;
41 private final Connection connection
;
42 private final JobExecutor jobExecutor
;
44 public AccountRecordProcessor(
45 SignalAccount account
,
46 Connection connection
,
47 final JobExecutor jobExecutor
48 ) throws SQLException
{
49 this.account
= account
;
50 this.connection
= connection
;
51 this.jobExecutor
= jobExecutor
;
52 final var selfRecipientId
= account
.getSelfRecipientId();
53 final var recipient
= account
.getRecipientStore().getRecipient(connection
, selfRecipientId
);
54 final var storageId
= account
.getRecipientStore().getSelfStorageId(connection
);
55 this.localAccountRecord
= new SignalAccountRecord(storageId
,
56 StorageSyncModels
.localToRemoteRecord(connection
,
57 account
.getConfigurationStore(),
59 account
.getUsernameLink()));
63 protected boolean isInvalid(SignalAccountRecord remote
) {
68 protected Optional
<SignalAccountRecord
> getMatching(SignalAccountRecord
record) {
69 return Optional
.of(localAccountRecord
);
73 protected SignalAccountRecord
merge(SignalAccountRecord remoteRecord
, SignalAccountRecord localRecord
) {
74 final var remote
= remoteRecord
.getProto();
75 final var local
= localRecord
.getProto();
78 if (!remote
.givenName
.isEmpty() || !remote
.familyName
.isEmpty()) {
79 givenName
= remote
.givenName
;
80 familyName
= remote
.familyName
;
82 givenName
= local
.givenName
;
83 familyName
= local
.familyName
;
86 final var payments
= remote
.payments
!= null && remote
.payments
.entropy
.size() > 0
90 final ByteString donationSubscriberId
;
91 final String donationSubscriberCurrencyCode
;
93 if (remote
.subscriberId
.size() > 0) {
94 donationSubscriberId
= remote
.subscriberId
;
95 donationSubscriberCurrencyCode
= remote
.subscriberCurrencyCode
;
97 donationSubscriberId
= local
.subscriberId
;
98 donationSubscriberCurrencyCode
= local
.subscriberCurrencyCode
;
101 final ByteString backupsSubscriberId
;
102 final IAPSubscriptionId backupsPurchaseToken
;
104 final var remoteBackupSubscriberData
= remote
.backupSubscriberData
;
105 if (remoteBackupSubscriberData
!= null && remoteBackupSubscriberData
.subscriberId
.size() > 0) {
106 backupsSubscriberId
= remoteBackupSubscriberData
.subscriberId
;
107 backupsPurchaseToken
= IAPSubscriptionId
.Companion
.from(remoteBackupSubscriberData
);
109 backupsSubscriberId
= local
.backupSubscriberData
!= null
110 ? local
.backupSubscriberData
.subscriberId
112 backupsPurchaseToken
= IAPSubscriptionId
.Companion
.from(local
.backupSubscriberData
);
115 final var mergedBuilder
= SignalAccountRecord
.Companion
.newBuilder(remote
.unknownFields().toByteArray())
116 .givenName(givenName
)
117 .familyName(familyName
)
118 .avatarUrlPath(firstNonEmpty(remote
.avatarUrlPath
, local
.avatarUrlPath
))
119 .profileKey(firstNonEmpty(remote
.profileKey
, local
.profileKey
))
120 .noteToSelfArchived(remote
.noteToSelfArchived
)
121 .noteToSelfMarkedUnread(remote
.noteToSelfMarkedUnread
)
122 .readReceipts(remote
.readReceipts
)
123 .typingIndicators(remote
.typingIndicators
)
124 .sealedSenderIndicators(remote
.sealedSenderIndicators
)
125 .linkPreviews(remote
.linkPreviews
)
126 .unlistedPhoneNumber(remote
.unlistedPhoneNumber
)
127 .phoneNumberSharingMode(remote
.phoneNumberSharingMode
)
128 .pinnedConversations(remote
.pinnedConversations
)
129 .preferContactAvatars(remote
.preferContactAvatars
)
130 .universalExpireTimer(remote
.universalExpireTimer
)
131 .preferredReactionEmoji(firstNonEmpty(remote
.preferredReactionEmoji
, local
.preferredReactionEmoji
))
132 .subscriberId(firstNonEmpty(remote
.subscriberId
, local
.subscriberId
))
133 .subscriberCurrencyCode(firstNonEmpty(remote
.subscriberCurrencyCode
, local
.subscriberCurrencyCode
))
134 .displayBadgesOnProfile(remote
.displayBadgesOnProfile
)
135 .subscriptionManuallyCancelled(remote
.subscriptionManuallyCancelled
)
136 .keepMutedChatsArchived(remote
.keepMutedChatsArchived
)
137 .hasSetMyStoriesPrivacy(remote
.hasSetMyStoriesPrivacy
)
138 .hasViewedOnboardingStory(remote
.hasViewedOnboardingStory
|| local
.hasViewedOnboardingStory
)
139 .storiesDisabled(remote
.storiesDisabled
)
140 .hasSeenGroupStoryEducationSheet(remote
.hasSeenGroupStoryEducationSheet
141 || local
.hasSeenGroupStoryEducationSheet
)
142 .hasCompletedUsernameOnboarding(remote
.hasCompletedUsernameOnboarding
143 || local
.hasCompletedUsernameOnboarding
)
144 .storyViewReceiptsEnabled(remote
.storyViewReceiptsEnabled
== OptionalBool
.UNSET
145 ? local
.storyViewReceiptsEnabled
146 : remote
.storyViewReceiptsEnabled
)
147 .username(remote
.username
)
148 .usernameLink(remote
.usernameLink
)
149 .e164(account
.isPrimaryDevice() ? local
.e164
: remote
.e164
);
150 safeSetPayments(mergedBuilder
,
151 payments
!= null && payments
.enabled
,
152 payments
== null ?
null : payments
.entropy
.toByteArray());
153 safeSetSubscriber(mergedBuilder
, donationSubscriberId
, donationSubscriberCurrencyCode
);
154 safeSetBackupsSubscriber(mergedBuilder
, backupsSubscriberId
, backupsPurchaseToken
);
156 final var merged
= mergedBuilder
.build();
158 final var matchesRemote
= doProtosMatch(merged
, remote
);
163 final var matchesLocal
= doProtosMatch(merged
, local
);
168 return new SignalAccountRecord(StorageId
.forAccount(KeyUtils
.createRawStorageId()), mergedBuilder
.build());
172 protected void insertLocal(SignalAccountRecord
record) {
173 throw new UnsupportedOperationException(
174 "We should always have a local AccountRecord, so we should never been inserting a new one.");
178 protected void updateLocal(StorageRecordUpdate
<SignalAccountRecord
> update
) throws SQLException
{
179 final var accountRecord
= update
.newRecord();
180 final var accountProto
= accountRecord
.getProto();
182 if (!accountProto
.e164
.equals(account
.getNumber())) {
183 jobExecutor
.enqueueJob(new CheckWhoAmIJob());
186 account
.getConfigurationStore().setReadReceipts(connection
, accountProto
.readReceipts
);
187 account
.getConfigurationStore().setTypingIndicators(connection
, accountProto
.typingIndicators
);
188 account
.getConfigurationStore()
189 .setUnidentifiedDeliveryIndicators(connection
, accountProto
.sealedSenderIndicators
);
190 account
.getConfigurationStore().setLinkPreviews(connection
, accountProto
.linkPreviews
);
191 account
.getConfigurationStore()
192 .setPhoneNumberSharingMode(connection
,
193 StorageSyncModels
.remoteToLocal(accountProto
.phoneNumberSharingMode
));
194 account
.getConfigurationStore().setPhoneNumberUnlisted(connection
, accountProto
.unlistedPhoneNumber
);
196 account
.setUsername(!accountProto
.username
.isEmpty() ? accountProto
.username
: null);
197 if (accountProto
.usernameLink
!= null) {
198 final var usernameLink
= accountProto
.usernameLink
;
199 account
.setUsernameLink(new UsernameLinkComponents(usernameLink
.entropy
.toByteArray(),
200 UuidUtil
.parseOrThrow(usernameLink
.serverId
.toByteArray())));
201 account
.getConfigurationStore().setUsernameLinkColor(connection
, usernameLink
.color
.name());
204 if (accountProto
.profileKey
.size() > 0) {
205 ProfileKey profileKey
;
207 profileKey
= new ProfileKey(accountProto
.profileKey
.toByteArray());
208 } catch (InvalidInputException e
) {
209 logger
.debug("Received invalid profile key from storage");
212 if (profileKey
!= null) {
213 account
.setProfileKey(profileKey
);
214 final var avatarPath
= accountProto
.avatarUrlPath
.isEmpty() ?
null : accountProto
.avatarUrlPath
;
215 jobExecutor
.enqueueJob(new DownloadProfileAvatarJob(avatarPath
));
219 final var profile
= account
.getRecipientStore().getProfile(connection
, account
.getSelfRecipientId());
220 final var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
221 builder
.withGivenName(accountProto
.givenName
);
222 builder
.withFamilyName(accountProto
.familyName
);
223 account
.getRecipientStore().storeProfile(connection
, account
.getSelfRecipientId(), builder
.build());
224 account
.getRecipientStore()
225 .storeStorageRecord(connection
,
226 account
.getSelfRecipientId(),
227 accountRecord
.getId(),
228 accountProto
.encode());
232 public int compare(SignalAccountRecord lhs
, SignalAccountRecord rhs
) {
236 private static boolean doProtosMatch(AccountRecord merged
, AccountRecord other
) {
237 return Arrays
.equals(merged
.encode(), other
.encode());