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
.DownloadProfileAvatarJob
;
6 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
7 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
8 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
9 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
10 import org
.slf4j
.Logger
;
11 import org
.slf4j
.LoggerFactory
;
12 import org
.whispersystems
.signalservice
.api
.push
.UsernameLinkComponents
;
13 import org
.whispersystems
.signalservice
.api
.storage
.IAPSubscriptionId
;
14 import org
.whispersystems
.signalservice
.api
.storage
.SignalAccountRecord
;
15 import org
.whispersystems
.signalservice
.api
.storage
.StorageId
;
16 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
17 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.AccountRecord
;
18 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.OptionalBool
;
20 import java
.sql
.Connection
;
21 import java
.sql
.SQLException
;
22 import java
.util
.Arrays
;
23 import java
.util
.Optional
;
25 import okio
.ByteString
;
27 import static org
.asamk
.signal
.manager
.util
.Utils
.firstNonEmpty
;
28 import static org
.whispersystems
.signalservice
.api
.storage
.AccountRecordExtensionsKt
.safeSetBackupsSubscriber
;
29 import static org
.whispersystems
.signalservice
.api
.storage
.AccountRecordExtensionsKt
.safeSetPayments
;
30 import static org
.whispersystems
.signalservice
.api
.storage
.AccountRecordExtensionsKt
.safeSetSubscriber
;
33 * Processes {@link SignalAccountRecord}s.
35 public class AccountRecordProcessor
extends DefaultStorageRecordProcessor
<SignalAccountRecord
> {
37 private static final Logger logger
= LoggerFactory
.getLogger(AccountRecordProcessor
.class);
38 private final SignalAccountRecord localAccountRecord
;
39 private final SignalAccount account
;
40 private final Connection connection
;
41 private final JobExecutor jobExecutor
;
43 public AccountRecordProcessor(
44 SignalAccount account
,
45 Connection connection
,
46 final JobExecutor jobExecutor
47 ) throws SQLException
{
48 this.account
= account
;
49 this.connection
= connection
;
50 this.jobExecutor
= jobExecutor
;
51 final var selfRecipientId
= account
.getSelfRecipientId();
52 final var recipient
= account
.getRecipientStore().getRecipient(connection
, selfRecipientId
);
53 final var storageId
= account
.getRecipientStore().getSelfStorageId(connection
);
54 this.localAccountRecord
= new SignalAccountRecord(storageId
,
55 StorageSyncModels
.localToRemoteRecord(connection
,
56 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 .avatarColor(remote
.avatarColor
);
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 account
.getConfigurationStore().setReadReceipts(connection
, accountProto
.readReceipts
);
182 account
.getConfigurationStore().setTypingIndicators(connection
, accountProto
.typingIndicators
);
183 account
.getConfigurationStore()
184 .setUnidentifiedDeliveryIndicators(connection
, accountProto
.sealedSenderIndicators
);
185 account
.getConfigurationStore().setLinkPreviews(connection
, accountProto
.linkPreviews
);
186 account
.getConfigurationStore()
187 .setPhoneNumberSharingMode(connection
,
188 StorageSyncModels
.remoteToLocal(accountProto
.phoneNumberSharingMode
));
189 account
.getConfigurationStore().setPhoneNumberUnlisted(connection
, accountProto
.unlistedPhoneNumber
);
191 account
.setUsername(!accountProto
.username
.isEmpty() ? accountProto
.username
: null);
192 if (accountProto
.usernameLink
!= null) {
193 final var usernameLink
= accountProto
.usernameLink
;
194 account
.setUsernameLink(new UsernameLinkComponents(usernameLink
.entropy
.toByteArray(),
195 UuidUtil
.parseOrThrow(usernameLink
.serverId
.toByteArray())));
196 account
.getConfigurationStore().setUsernameLinkColor(connection
, usernameLink
.color
.name());
199 if (accountProto
.profileKey
.size() > 0) {
200 ProfileKey profileKey
;
202 profileKey
= new ProfileKey(accountProto
.profileKey
.toByteArray());
203 } catch (InvalidInputException e
) {
204 logger
.debug("Received invalid profile key from storage");
207 if (profileKey
!= null) {
208 account
.setProfileKey(profileKey
);
209 final var avatarPath
= accountProto
.avatarUrlPath
.isEmpty() ?
null : accountProto
.avatarUrlPath
;
210 jobExecutor
.enqueueJob(new DownloadProfileAvatarJob(avatarPath
));
214 final var profile
= account
.getRecipientStore().getProfile(connection
, account
.getSelfRecipientId());
215 final var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
216 builder
.withGivenName(accountProto
.givenName
);
217 builder
.withFamilyName(accountProto
.familyName
);
218 account
.getRecipientStore().storeProfile(connection
, account
.getSelfRecipientId(), builder
.build());
219 account
.getRecipientStore()
220 .storeStorageRecord(connection
,
221 account
.getSelfRecipientId(),
222 accountRecord
.getId(),
223 accountProto
.encode());
227 public int compare(SignalAccountRecord lhs
, SignalAccountRecord rhs
) {
231 private static boolean doProtosMatch(AccountRecord merged
, AccountRecord other
) {
232 return Arrays
.equals(merged
.encode(), other
.encode());