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(account
.getConfigurationStore(),
58 account
.getUsernameLink()));
62 protected boolean isInvalid(SignalAccountRecord remote
) {
67 protected Optional
<SignalAccountRecord
> getMatching(SignalAccountRecord
record) {
68 return Optional
.of(localAccountRecord
);
72 protected SignalAccountRecord
merge(SignalAccountRecord remoteRecord
, SignalAccountRecord localRecord
) {
73 final var remote
= remoteRecord
.getProto();
74 final var local
= localRecord
.getProto();
77 if (!remote
.givenName
.isEmpty() || !remote
.familyName
.isEmpty()) {
78 givenName
= remote
.givenName
;
79 familyName
= remote
.familyName
;
81 givenName
= local
.givenName
;
82 familyName
= local
.familyName
;
85 final var payments
= remote
.payments
!= null && remote
.payments
.entropy
.size() > 0
89 final ByteString donationSubscriberId
;
90 final String donationSubscriberCurrencyCode
;
92 if (remote
.subscriberId
.size() > 0) {
93 donationSubscriberId
= remote
.subscriberId
;
94 donationSubscriberCurrencyCode
= remote
.subscriberCurrencyCode
;
96 donationSubscriberId
= local
.subscriberId
;
97 donationSubscriberCurrencyCode
= local
.subscriberCurrencyCode
;
100 final ByteString backupsSubscriberId
;
101 final IAPSubscriptionId backupsPurchaseToken
;
103 final var remoteBackupSubscriberData
= remote
.backupSubscriberData
;
104 if (remoteBackupSubscriberData
!= null && remoteBackupSubscriberData
.subscriberId
.size() > 0) {
105 backupsSubscriberId
= remoteBackupSubscriberData
.subscriberId
;
106 backupsPurchaseToken
= IAPSubscriptionId
.Companion
.from(remoteBackupSubscriberData
);
108 backupsSubscriberId
= local
.backupSubscriberData
!= null
109 ? local
.backupSubscriberData
.subscriberId
111 backupsPurchaseToken
= IAPSubscriptionId
.Companion
.from(local
.backupSubscriberData
);
114 final var mergedBuilder
= SignalAccountRecord
.Companion
.newBuilder(remote
.unknownFields().toByteArray())
115 .givenName(givenName
)
116 .familyName(familyName
)
117 .avatarUrlPath(firstNonEmpty(remote
.avatarUrlPath
, local
.avatarUrlPath
))
118 .profileKey(firstNonEmpty(remote
.profileKey
, local
.profileKey
))
119 .noteToSelfArchived(remote
.noteToSelfArchived
)
120 .noteToSelfMarkedUnread(remote
.noteToSelfMarkedUnread
)
121 .readReceipts(remote
.readReceipts
)
122 .typingIndicators(remote
.typingIndicators
)
123 .sealedSenderIndicators(remote
.sealedSenderIndicators
)
124 .linkPreviews(remote
.linkPreviews
)
125 .unlistedPhoneNumber(remote
.unlistedPhoneNumber
)
126 .phoneNumberSharingMode(remote
.phoneNumberSharingMode
)
127 .pinnedConversations(remote
.pinnedConversations
)
128 .preferContactAvatars(remote
.preferContactAvatars
)
129 .universalExpireTimer(remote
.universalExpireTimer
)
130 .preferredReactionEmoji(firstNonEmpty(remote
.preferredReactionEmoji
, local
.preferredReactionEmoji
))
131 .subscriberId(firstNonEmpty(remote
.subscriberId
, local
.subscriberId
))
132 .subscriberCurrencyCode(firstNonEmpty(remote
.subscriberCurrencyCode
, local
.subscriberCurrencyCode
))
133 .displayBadgesOnProfile(remote
.displayBadgesOnProfile
)
134 .subscriptionManuallyCancelled(remote
.subscriptionManuallyCancelled
)
135 .keepMutedChatsArchived(remote
.keepMutedChatsArchived
)
136 .hasSetMyStoriesPrivacy(remote
.hasSetMyStoriesPrivacy
)
137 .hasViewedOnboardingStory(remote
.hasViewedOnboardingStory
|| local
.hasViewedOnboardingStory
)
138 .storiesDisabled(remote
.storiesDisabled
)
139 .hasSeenGroupStoryEducationSheet(remote
.hasSeenGroupStoryEducationSheet
140 || local
.hasSeenGroupStoryEducationSheet
)
141 .hasCompletedUsernameOnboarding(remote
.hasCompletedUsernameOnboarding
142 || local
.hasCompletedUsernameOnboarding
)
143 .storyViewReceiptsEnabled(remote
.storyViewReceiptsEnabled
== OptionalBool
.UNSET
144 ? local
.storyViewReceiptsEnabled
145 : remote
.storyViewReceiptsEnabled
)
146 .username(remote
.username
)
147 .usernameLink(remote
.usernameLink
)
148 .e164(account
.isPrimaryDevice() ? local
.e164
: remote
.e164
);
149 safeSetPayments(mergedBuilder
,
150 payments
!= null && payments
.enabled
,
151 payments
== null ?
null : payments
.entropy
.toByteArray());
152 safeSetSubscriber(mergedBuilder
, donationSubscriberId
, donationSubscriberCurrencyCode
);
153 safeSetBackupsSubscriber(mergedBuilder
, backupsSubscriberId
, backupsPurchaseToken
);
155 final var merged
= mergedBuilder
.build();
157 final var matchesRemote
= doProtosMatch(merged
, remote
);
162 final var matchesLocal
= doProtosMatch(merged
, local
);
167 return new SignalAccountRecord(StorageId
.forAccount(KeyUtils
.createRawStorageId()), mergedBuilder
.build());
171 protected void insertLocal(SignalAccountRecord
record) {
172 throw new UnsupportedOperationException(
173 "We should always have a local AccountRecord, so we should never been inserting a new one.");
177 protected void updateLocal(StorageRecordUpdate
<SignalAccountRecord
> update
) throws SQLException
{
178 final var accountRecord
= update
.newRecord();
179 final var accountProto
= accountRecord
.getProto();
181 if (!accountProto
.e164
.equals(account
.getNumber())) {
182 jobExecutor
.enqueueJob(new CheckWhoAmIJob());
185 account
.getConfigurationStore().setReadReceipts(connection
, accountProto
.readReceipts
);
186 account
.getConfigurationStore().setTypingIndicators(connection
, accountProto
.typingIndicators
);
187 account
.getConfigurationStore()
188 .setUnidentifiedDeliveryIndicators(connection
, accountProto
.sealedSenderIndicators
);
189 account
.getConfigurationStore().setLinkPreviews(connection
, accountProto
.linkPreviews
);
190 account
.getConfigurationStore()
191 .setPhoneNumberSharingMode(connection
,
192 StorageSyncModels
.remoteToLocal(accountProto
.phoneNumberSharingMode
));
193 account
.getConfigurationStore().setPhoneNumberUnlisted(connection
, accountProto
.unlistedPhoneNumber
);
195 account
.setUsername(!accountProto
.username
.isEmpty() ? accountProto
.username
: null);
196 if (accountProto
.usernameLink
!= null) {
197 final var usernameLink
= accountProto
.usernameLink
;
198 account
.setUsernameLink(new UsernameLinkComponents(usernameLink
.entropy
.toByteArray(),
199 UuidUtil
.parseOrThrow(usernameLink
.serverId
.toByteArray())));
200 account
.getConfigurationStore().setUsernameLinkColor(connection
, usernameLink
.color
.name());
203 if (accountProto
.profileKey
.size() > 0) {
204 ProfileKey profileKey
;
206 profileKey
= new ProfileKey(accountProto
.profileKey
.toByteArray());
207 } catch (InvalidInputException e
) {
208 logger
.debug("Received invalid profile key from storage");
211 if (profileKey
!= null) {
212 account
.setProfileKey(profileKey
);
213 final var avatarPath
= accountProto
.avatarUrlPath
.isEmpty() ?
null : accountProto
.avatarUrlPath
;
214 jobExecutor
.enqueueJob(new DownloadProfileAvatarJob(avatarPath
));
218 final var profile
= account
.getRecipientStore().getProfile(connection
, account
.getSelfRecipientId());
219 final var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
220 builder
.withGivenName(accountProto
.givenName
);
221 builder
.withFamilyName(accountProto
.familyName
);
222 account
.getRecipientStore().storeProfile(connection
, account
.getSelfRecipientId(), builder
.build());
223 account
.getRecipientStore()
224 .storeStorageRecord(connection
,
225 account
.getSelfRecipientId(),
226 accountRecord
.getId(),
227 accountProto
.encode());
231 public int compare(SignalAccountRecord lhs
, SignalAccountRecord rhs
) {
235 private static boolean doProtosMatch(AccountRecord merged
, AccountRecord other
) {
236 return Arrays
.equals(merged
.encode(), other
.encode());