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
.util
.OptionalUtil
;
16 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
17 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.OptionalBool
;
19 import java
.sql
.Connection
;
20 import java
.sql
.SQLException
;
21 import java
.util
.Arrays
;
22 import java
.util
.Optional
;
25 * Processes {@link SignalAccountRecord}s.
27 public class AccountRecordProcessor
extends DefaultStorageRecordProcessor
<SignalAccountRecord
> {
29 private static final Logger logger
= LoggerFactory
.getLogger(AccountRecordProcessor
.class);
30 private final SignalAccountRecord localAccountRecord
;
31 private final SignalAccount account
;
32 private final Connection connection
;
33 private final JobExecutor jobExecutor
;
35 public AccountRecordProcessor(
36 SignalAccount account
,
37 Connection connection
,
38 final JobExecutor jobExecutor
39 ) throws SQLException
{
40 this.account
= account
;
41 this.connection
= connection
;
42 this.jobExecutor
= jobExecutor
;
43 final var selfRecipientId
= account
.getSelfRecipientId();
44 final var recipient
= account
.getRecipientStore().getRecipient(connection
, selfRecipientId
);
45 final var storageId
= account
.getRecipientStore().getSelfStorageId(connection
);
46 this.localAccountRecord
= StorageSyncModels
.localToRemoteRecord(account
.getConfigurationStore(),
48 account
.getUsernameLink(),
49 storageId
.getRaw()).getAccount().get();
53 protected boolean isInvalid(SignalAccountRecord remote
) {
58 protected Optional
<SignalAccountRecord
> getMatching(SignalAccountRecord
record) {
59 return Optional
.of(localAccountRecord
);
63 protected SignalAccountRecord
merge(SignalAccountRecord remote
, SignalAccountRecord local
) {
66 if (remote
.getGivenName().isPresent() || remote
.getFamilyName().isPresent()) {
67 givenName
= remote
.getGivenName().orElse("");
68 familyName
= remote
.getFamilyName().orElse("");
70 givenName
= local
.getGivenName().orElse("");
71 familyName
= local
.getFamilyName().orElse("");
74 final var payments
= remote
.getPayments().getEntropy().isPresent() ? remote
.getPayments() : local
.getPayments();
75 final var subscriber
= remote
.getSubscriber().getId().isPresent()
76 ? remote
.getSubscriber()
77 : local
.getSubscriber();
78 final var storyViewReceiptsState
= remote
.getStoryViewReceiptsState() == OptionalBool
.UNSET
79 ? local
.getStoryViewReceiptsState()
80 : remote
.getStoryViewReceiptsState();
81 final var unknownFields
= remote
.serializeUnknownFields();
82 final var avatarUrlPath
= OptionalUtil
.or(remote
.getAvatarUrlPath(), local
.getAvatarUrlPath()).orElse("");
83 final var profileKey
= OptionalUtil
.or(remote
.getProfileKey(), local
.getProfileKey()).orElse(null);
84 final var noteToSelfArchived
= remote
.isNoteToSelfArchived();
85 final var noteToSelfForcedUnread
= remote
.isNoteToSelfForcedUnread();
86 final var readReceipts
= remote
.isReadReceiptsEnabled();
87 final var typingIndicators
= remote
.isTypingIndicatorsEnabled();
88 final var sealedSenderIndicators
= remote
.isSealedSenderIndicatorsEnabled();
89 final var linkPreviews
= remote
.isLinkPreviewsEnabled();
90 final var unlisted
= remote
.isPhoneNumberUnlisted();
91 final var pinnedConversations
= remote
.getPinnedConversations();
92 final var phoneNumberSharingMode
= remote
.getPhoneNumberSharingMode();
93 final var preferContactAvatars
= remote
.isPreferContactAvatars();
94 final var universalExpireTimer
= remote
.getUniversalExpireTimer();
95 final var e164
= account
.isPrimaryDevice() ? local
.getE164() : remote
.getE164();
96 final var defaultReactions
= !remote
.getDefaultReactions().isEmpty()
97 ? remote
.getDefaultReactions()
98 : local
.getDefaultReactions();
99 final var displayBadgesOnProfile
= remote
.isDisplayBadgesOnProfile();
100 final var subscriptionManuallyCancelled
= remote
.isSubscriptionManuallyCancelled();
101 final var keepMutedChatsArchived
= remote
.isKeepMutedChatsArchived();
102 final var hasSetMyStoriesPrivacy
= remote
.hasSetMyStoriesPrivacy();
103 final var hasViewedOnboardingStory
= remote
.hasViewedOnboardingStory() || local
.hasViewedOnboardingStory();
104 final var storiesDisabled
= remote
.isStoriesDisabled();
105 final var hasSeenGroupStoryEducation
= remote
.hasSeenGroupStoryEducationSheet()
106 || local
.hasSeenGroupStoryEducationSheet();
107 boolean hasSeenUsernameOnboarding
= remote
.hasCompletedUsernameOnboarding()
108 || local
.hasCompletedUsernameOnboarding();
109 final var username
= remote
.getUsername();
110 final var usernameLink
= remote
.getUsernameLink();
112 final var mergedBuilder
= new SignalAccountRecord
.Builder(remote
.getId().getRaw(), unknownFields
).setGivenName(
114 .setFamilyName(familyName
)
115 .setAvatarUrlPath(avatarUrlPath
)
116 .setProfileKey(profileKey
)
117 .setNoteToSelfArchived(noteToSelfArchived
)
118 .setNoteToSelfForcedUnread(noteToSelfForcedUnread
)
119 .setReadReceiptsEnabled(readReceipts
)
120 .setTypingIndicatorsEnabled(typingIndicators
)
121 .setSealedSenderIndicatorsEnabled(sealedSenderIndicators
)
122 .setLinkPreviewsEnabled(linkPreviews
)
123 .setUnlistedPhoneNumber(unlisted
)
124 .setPhoneNumberSharingMode(phoneNumberSharingMode
)
125 .setPinnedConversations(pinnedConversations
)
126 .setPreferContactAvatars(preferContactAvatars
)
127 .setPayments(payments
.isEnabled(), payments
.getEntropy().orElse(null))
128 .setUniversalExpireTimer(universalExpireTimer
)
129 .setDefaultReactions(defaultReactions
)
130 .setSubscriber(subscriber
)
131 .setDisplayBadgesOnProfile(displayBadgesOnProfile
)
132 .setSubscriptionManuallyCancelled(subscriptionManuallyCancelled
)
133 .setKeepMutedChatsArchived(keepMutedChatsArchived
)
134 .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy
)
135 .setHasViewedOnboardingStory(hasViewedOnboardingStory
)
136 .setStoriesDisabled(storiesDisabled
)
137 .setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation
)
138 .setHasCompletedUsernameOnboarding(hasSeenUsernameOnboarding
)
139 .setStoryViewReceiptsState(storyViewReceiptsState
)
140 .setUsername(username
)
141 .setUsernameLink(usernameLink
)
143 final var merged
= mergedBuilder
.build();
145 final var matchesRemote
= doProtosMatch(merged
, remote
);
150 final var matchesLocal
= doProtosMatch(merged
, local
);
155 return mergedBuilder
.setId(KeyUtils
.createRawStorageId()).build();
159 protected void insertLocal(SignalAccountRecord
record) {
160 throw new UnsupportedOperationException(
161 "We should always have a local AccountRecord, so we should never been inserting a new one.");
165 protected void updateLocal(StorageRecordUpdate
<SignalAccountRecord
> update
) throws SQLException
{
166 final var accountRecord
= update
.newRecord();
168 if (!accountRecord
.getE164().equals(account
.getNumber())) {
169 jobExecutor
.enqueueJob(new CheckWhoAmIJob());
172 account
.getConfigurationStore().setReadReceipts(connection
, accountRecord
.isReadReceiptsEnabled());
173 account
.getConfigurationStore().setTypingIndicators(connection
, accountRecord
.isTypingIndicatorsEnabled());
174 account
.getConfigurationStore()
175 .setUnidentifiedDeliveryIndicators(connection
, accountRecord
.isSealedSenderIndicatorsEnabled());
176 account
.getConfigurationStore().setLinkPreviews(connection
, accountRecord
.isLinkPreviewsEnabled());
177 account
.getConfigurationStore()
178 .setPhoneNumberSharingMode(connection
,
179 StorageSyncModels
.remoteToLocal(accountRecord
.getPhoneNumberSharingMode()));
180 account
.getConfigurationStore().setPhoneNumberUnlisted(connection
, accountRecord
.isPhoneNumberUnlisted());
182 account
.setUsername(accountRecord
.getUsername() != null && !accountRecord
.getUsername().isEmpty()
183 ? accountRecord
.getUsername()
185 if (accountRecord
.getUsernameLink() != null) {
186 final var usernameLink
= accountRecord
.getUsernameLink();
187 account
.setUsernameLink(new UsernameLinkComponents(usernameLink
.entropy
.toByteArray(),
188 UuidUtil
.parseOrThrow(usernameLink
.serverId
.toByteArray())));
189 account
.getConfigurationStore().setUsernameLinkColor(connection
, usernameLink
.color
.name());
192 if (accountRecord
.getProfileKey().isPresent()) {
193 ProfileKey profileKey
;
195 profileKey
= new ProfileKey(accountRecord
.getProfileKey().get());
196 } catch (InvalidInputException e
) {
197 logger
.debug("Received invalid profile key from storage");
200 if (profileKey
!= null) {
201 account
.setProfileKey(profileKey
);
202 final var avatarPath
= accountRecord
.getAvatarUrlPath().orElse(null);
203 jobExecutor
.enqueueJob(new DownloadProfileAvatarJob(avatarPath
));
207 final var profile
= account
.getRecipientStore().getProfile(connection
, account
.getSelfRecipientId());
208 final var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
209 builder
.withGivenName(accountRecord
.getGivenName().orElse(null));
210 builder
.withFamilyName(accountRecord
.getFamilyName().orElse(null));
211 account
.getRecipientStore().storeProfile(connection
, account
.getSelfRecipientId(), builder
.build());
212 account
.getRecipientStore()
213 .storeStorageRecord(connection
,
214 account
.getSelfRecipientId(),
215 accountRecord
.getId(),
216 accountRecord
.toProto().encode());
220 public int compare(SignalAccountRecord lhs
, SignalAccountRecord rhs
) {
224 private static boolean doProtosMatch(SignalAccountRecord merged
, SignalAccountRecord other
) {
225 return Arrays
.equals(merged
.toProto().encode(), other
.toProto().encode());