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
.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 static org
.asamk
.signal
.manager
.util
.Utils
.firstNonEmpty
;
26 import static org
.asamk
.signal
.manager
.util
.Utils
.firstNonNull
;
29 * Processes {@link SignalAccountRecord}s.
31 public class AccountRecordProcessor
extends DefaultStorageRecordProcessor
<SignalAccountRecord
> {
33 private static final Logger logger
= LoggerFactory
.getLogger(AccountRecordProcessor
.class);
34 private final SignalAccountRecord localAccountRecord
;
35 private final SignalAccount account
;
36 private final Connection connection
;
37 private final JobExecutor jobExecutor
;
39 public AccountRecordProcessor(
40 SignalAccount account
,
41 Connection connection
,
42 final JobExecutor jobExecutor
43 ) throws SQLException
{
44 this.account
= account
;
45 this.connection
= connection
;
46 this.jobExecutor
= jobExecutor
;
47 final var selfRecipientId
= account
.getSelfRecipientId();
48 final var recipient
= account
.getRecipientStore().getRecipient(connection
, selfRecipientId
);
49 final var storageId
= account
.getRecipientStore().getSelfStorageId(connection
);
50 this.localAccountRecord
= new SignalAccountRecord(storageId
,
51 StorageSyncModels
.localToRemoteRecord(account
.getConfigurationStore(),
53 account
.getUsernameLink()));
57 protected boolean isInvalid(SignalAccountRecord remote
) {
62 protected Optional
<SignalAccountRecord
> getMatching(SignalAccountRecord
record) {
63 return Optional
.of(localAccountRecord
);
67 protected SignalAccountRecord
merge(SignalAccountRecord remoteRecord
, SignalAccountRecord localRecord
) {
68 final var remote
= remoteRecord
.getProto();
69 final var local
= localRecord
.getProto();
72 if (!remote
.givenName
.isEmpty() || !remote
.familyName
.isEmpty()) {
73 givenName
= remote
.givenName
;
74 familyName
= remote
.familyName
;
76 givenName
= local
.givenName
;
77 familyName
= local
.familyName
;
80 final var mergedBuilder
= SignalAccountRecord
.Companion
.newBuilder(remote
.unknownFields().toByteArray())
82 .familyName(familyName
)
83 .avatarUrlPath(firstNonEmpty(remote
.avatarUrlPath
, local
.avatarUrlPath
))
84 .profileKey(firstNonEmpty(remote
.profileKey
, local
.profileKey
))
85 .noteToSelfArchived(remote
.noteToSelfArchived
)
86 .noteToSelfMarkedUnread(remote
.noteToSelfMarkedUnread
)
87 .readReceipts(remote
.readReceipts
)
88 .typingIndicators(remote
.typingIndicators
)
89 .sealedSenderIndicators(remote
.sealedSenderIndicators
)
90 .linkPreviews(remote
.linkPreviews
)
91 .unlistedPhoneNumber(remote
.unlistedPhoneNumber
)
92 .phoneNumberSharingMode(remote
.phoneNumberSharingMode
)
93 .pinnedConversations(remote
.pinnedConversations
)
94 .preferContactAvatars(remote
.preferContactAvatars
)
95 .universalExpireTimer(remote
.universalExpireTimer
)
96 .preferredReactionEmoji(firstNonEmpty(remote
.preferredReactionEmoji
, local
.preferredReactionEmoji
))
97 .subscriberId(firstNonEmpty(remote
.subscriberId
, local
.subscriberId
))
98 .subscriberCurrencyCode(firstNonEmpty(remote
.subscriberCurrencyCode
, local
.subscriberCurrencyCode
))
99 .backupsSubscriberId(firstNonEmpty(remote
.backupsSubscriberId
, local
.backupsSubscriberId
))
100 .backupsSubscriberCurrencyCode(firstNonEmpty(remote
.backupsSubscriberCurrencyCode
,
101 local
.backupsSubscriberCurrencyCode
))
102 .displayBadgesOnProfile(remote
.displayBadgesOnProfile
)
103 .subscriptionManuallyCancelled(remote
.subscriptionManuallyCancelled
)
104 .keepMutedChatsArchived(remote
.keepMutedChatsArchived
)
105 .hasSetMyStoriesPrivacy(remote
.hasSetMyStoriesPrivacy
)
106 .hasViewedOnboardingStory(remote
.hasViewedOnboardingStory
|| local
.hasViewedOnboardingStory
)
107 .storiesDisabled(remote
.storiesDisabled
)
108 .hasSeenGroupStoryEducationSheet(remote
.hasSeenGroupStoryEducationSheet
109 || local
.hasSeenGroupStoryEducationSheet
)
110 .hasCompletedUsernameOnboarding(remote
.hasCompletedUsernameOnboarding
111 || local
.hasCompletedUsernameOnboarding
)
112 .storyViewReceiptsEnabled(remote
.storyViewReceiptsEnabled
== OptionalBool
.UNSET
113 ? local
.storyViewReceiptsEnabled
114 : remote
.storyViewReceiptsEnabled
)
115 .username(remote
.username
)
116 .usernameLink(remote
.usernameLink
)
117 .e164(account
.isPrimaryDevice() ? local
.e164
: remote
.e164
);
118 if (firstNonNull(remote
.payments
, local
.payments
) != null) {
119 mergedBuilder
.payments(firstNonNull(remote
.payments
, local
.payments
));
121 final var merged
= mergedBuilder
.build();
123 final var matchesRemote
= doProtosMatch(merged
, remote
);
128 final var matchesLocal
= doProtosMatch(merged
, local
);
133 return new SignalAccountRecord(StorageId
.forAccount(KeyUtils
.createRawStorageId()), mergedBuilder
.build());
137 protected void insertLocal(SignalAccountRecord
record) {
138 throw new UnsupportedOperationException(
139 "We should always have a local AccountRecord, so we should never been inserting a new one.");
143 protected void updateLocal(StorageRecordUpdate
<SignalAccountRecord
> update
) throws SQLException
{
144 final var accountRecord
= update
.newRecord();
145 final var accountProto
= accountRecord
.getProto();
147 if (!accountProto
.e164
.equals(account
.getNumber())) {
148 jobExecutor
.enqueueJob(new CheckWhoAmIJob());
151 account
.getConfigurationStore().setReadReceipts(connection
, accountProto
.readReceipts
);
152 account
.getConfigurationStore().setTypingIndicators(connection
, accountProto
.typingIndicators
);
153 account
.getConfigurationStore()
154 .setUnidentifiedDeliveryIndicators(connection
, accountProto
.sealedSenderIndicators
);
155 account
.getConfigurationStore().setLinkPreviews(connection
, accountProto
.linkPreviews
);
156 account
.getConfigurationStore()
157 .setPhoneNumberSharingMode(connection
,
158 StorageSyncModels
.remoteToLocal(accountProto
.phoneNumberSharingMode
));
159 account
.getConfigurationStore().setPhoneNumberUnlisted(connection
, accountProto
.unlistedPhoneNumber
);
161 account
.setUsername(!accountProto
.username
.isEmpty() ? accountProto
.username
: null);
162 if (accountProto
.usernameLink
!= null) {
163 final var usernameLink
= accountProto
.usernameLink
;
164 account
.setUsernameLink(new UsernameLinkComponents(usernameLink
.entropy
.toByteArray(),
165 UuidUtil
.parseOrThrow(usernameLink
.serverId
.toByteArray())));
166 account
.getConfigurationStore().setUsernameLinkColor(connection
, usernameLink
.color
.name());
169 if (accountProto
.profileKey
.size() > 0) {
170 ProfileKey profileKey
;
172 profileKey
= new ProfileKey(accountProto
.profileKey
.toByteArray());
173 } catch (InvalidInputException e
) {
174 logger
.debug("Received invalid profile key from storage");
177 if (profileKey
!= null) {
178 account
.setProfileKey(profileKey
);
179 final var avatarPath
= accountProto
.avatarUrlPath
.isEmpty() ?
null : accountProto
.avatarUrlPath
;
180 jobExecutor
.enqueueJob(new DownloadProfileAvatarJob(avatarPath
));
184 final var profile
= account
.getRecipientStore().getProfile(connection
, account
.getSelfRecipientId());
185 final var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
186 builder
.withGivenName(accountProto
.givenName
);
187 builder
.withFamilyName(accountProto
.familyName
);
188 account
.getRecipientStore().storeProfile(connection
, account
.getSelfRecipientId(), builder
.build());
189 account
.getRecipientStore()
190 .storeStorageRecord(connection
,
191 account
.getSelfRecipientId(),
192 accountRecord
.getId(),
193 accountProto
.encode());
197 public int compare(SignalAccountRecord lhs
, SignalAccountRecord rhs
) {
201 private static boolean doProtosMatch(AccountRecord merged
, AccountRecord other
) {
202 return Arrays
.equals(merged
.encode(), other
.encode());