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
, Connection connection
, final JobExecutor jobExecutor
37 ) throws SQLException
{
38 this.account
= account
;
39 this.connection
= connection
;
40 this.jobExecutor
= jobExecutor
;
41 final var selfRecipientId
= account
.getSelfRecipientId();
42 final var recipient
= account
.getRecipientStore().getRecipient(connection
, selfRecipientId
);
43 final var storageId
= account
.getRecipientStore().getSelfStorageId(connection
);
44 this.localAccountRecord
= StorageSyncModels
.localToRemoteRecord(account
.getConfigurationStore(),
46 account
.getUsernameLink(),
47 storageId
.getRaw()).getAccount().get();
51 protected boolean isInvalid(SignalAccountRecord remote
) {
56 protected Optional
<SignalAccountRecord
> getMatching(SignalAccountRecord
record) {
57 return Optional
.of(localAccountRecord
);
61 protected SignalAccountRecord
merge(SignalAccountRecord remote
, SignalAccountRecord local
) {
64 if (remote
.getGivenName().isPresent() || remote
.getFamilyName().isPresent()) {
65 givenName
= remote
.getGivenName().orElse("");
66 familyName
= remote
.getFamilyName().orElse("");
68 givenName
= local
.getGivenName().orElse("");
69 familyName
= local
.getFamilyName().orElse("");
72 final var payments
= remote
.getPayments().getEntropy().isPresent() ? remote
.getPayments() : local
.getPayments();
73 final var subscriber
= remote
.getSubscriber().getId().isPresent()
74 ? remote
.getSubscriber()
75 : local
.getSubscriber();
76 final var storyViewReceiptsState
= remote
.getStoryViewReceiptsState() == OptionalBool
.UNSET
77 ? local
.getStoryViewReceiptsState()
78 : remote
.getStoryViewReceiptsState();
79 final var unknownFields
= remote
.serializeUnknownFields();
80 final var avatarUrlPath
= OptionalUtil
.or(remote
.getAvatarUrlPath(), local
.getAvatarUrlPath()).orElse("");
81 final var profileKey
= OptionalUtil
.or(remote
.getProfileKey(), local
.getProfileKey()).orElse(null);
82 final var noteToSelfArchived
= remote
.isNoteToSelfArchived();
83 final var noteToSelfForcedUnread
= remote
.isNoteToSelfForcedUnread();
84 final var readReceipts
= remote
.isReadReceiptsEnabled();
85 final var typingIndicators
= remote
.isTypingIndicatorsEnabled();
86 final var sealedSenderIndicators
= remote
.isSealedSenderIndicatorsEnabled();
87 final var linkPreviews
= remote
.isLinkPreviewsEnabled();
88 final var unlisted
= remote
.isPhoneNumberUnlisted();
89 final var pinnedConversations
= remote
.getPinnedConversations();
90 final var phoneNumberSharingMode
= remote
.getPhoneNumberSharingMode();
91 final var preferContactAvatars
= remote
.isPreferContactAvatars();
92 final var universalExpireTimer
= remote
.getUniversalExpireTimer();
93 final var e164
= local
.getE164();
94 final var defaultReactions
= !remote
.getDefaultReactions().isEmpty()
95 ? remote
.getDefaultReactions()
96 : local
.getDefaultReactions();
97 final var displayBadgesOnProfile
= remote
.isDisplayBadgesOnProfile();
98 final var subscriptionManuallyCancelled
= remote
.isSubscriptionManuallyCancelled();
99 final var keepMutedChatsArchived
= remote
.isKeepMutedChatsArchived();
100 final var hasSetMyStoriesPrivacy
= remote
.hasSetMyStoriesPrivacy();
101 final var hasViewedOnboardingStory
= remote
.hasViewedOnboardingStory() || local
.hasViewedOnboardingStory();
102 final var storiesDisabled
= remote
.isStoriesDisabled();
103 final var hasSeenGroupStoryEducation
= remote
.hasSeenGroupStoryEducationSheet()
104 || local
.hasSeenGroupStoryEducationSheet();
105 final var username
= remote
.getUsername();
106 final var usernameLink
= remote
.getUsernameLink();
108 final var mergedBuilder
= new SignalAccountRecord
.Builder(remote
.getId().getRaw(), unknownFields
).setGivenName(
110 .setFamilyName(familyName
)
111 .setAvatarUrlPath(avatarUrlPath
)
112 .setProfileKey(profileKey
)
113 .setNoteToSelfArchived(noteToSelfArchived
)
114 .setNoteToSelfForcedUnread(noteToSelfForcedUnread
)
115 .setReadReceiptsEnabled(readReceipts
)
116 .setTypingIndicatorsEnabled(typingIndicators
)
117 .setSealedSenderIndicatorsEnabled(sealedSenderIndicators
)
118 .setLinkPreviewsEnabled(linkPreviews
)
119 .setUnlistedPhoneNumber(unlisted
)
120 .setPhoneNumberSharingMode(phoneNumberSharingMode
)
121 .setPinnedConversations(pinnedConversations
)
122 .setPreferContactAvatars(preferContactAvatars
)
123 .setPayments(payments
.isEnabled(), payments
.getEntropy().orElse(null))
124 .setUniversalExpireTimer(universalExpireTimer
)
125 .setDefaultReactions(defaultReactions
)
126 .setSubscriber(subscriber
)
127 .setDisplayBadgesOnProfile(displayBadgesOnProfile
)
128 .setSubscriptionManuallyCancelled(subscriptionManuallyCancelled
)
129 .setKeepMutedChatsArchived(keepMutedChatsArchived
)
130 .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy
)
131 .setHasViewedOnboardingStory(hasViewedOnboardingStory
)
132 .setStoriesDisabled(storiesDisabled
)
133 .setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation
)
134 .setStoryViewReceiptsState(storyViewReceiptsState
)
135 .setUsername(username
)
136 .setUsernameLink(usernameLink
)
138 final var merged
= mergedBuilder
.build();
140 final var matchesRemote
= doProtosMatch(merged
, remote
);
145 final var matchesLocal
= doProtosMatch(merged
, local
);
150 return mergedBuilder
.setId(KeyUtils
.createRawStorageId()).build();
154 protected void insertLocal(SignalAccountRecord
record) {
155 throw new UnsupportedOperationException(
156 "We should always have a local AccountRecord, so we should never been inserting a new one.");
160 protected void updateLocal(StorageRecordUpdate
<SignalAccountRecord
> update
) throws SQLException
{
161 final var accountRecord
= update
.newRecord();
163 if (!accountRecord
.getE164().equals(account
.getNumber())) {
164 jobExecutor
.enqueueJob(new CheckWhoAmIJob());
167 account
.getConfigurationStore().setReadReceipts(connection
, accountRecord
.isReadReceiptsEnabled());
168 account
.getConfigurationStore().setTypingIndicators(connection
, accountRecord
.isTypingIndicatorsEnabled());
169 account
.getConfigurationStore()
170 .setUnidentifiedDeliveryIndicators(connection
, accountRecord
.isSealedSenderIndicatorsEnabled());
171 account
.getConfigurationStore().setLinkPreviews(connection
, accountRecord
.isLinkPreviewsEnabled());
172 account
.getConfigurationStore()
173 .setPhoneNumberSharingMode(connection
,
174 StorageSyncModels
.remoteToLocal(accountRecord
.getPhoneNumberSharingMode()));
175 account
.getConfigurationStore().setPhoneNumberUnlisted(connection
, accountRecord
.isPhoneNumberUnlisted());
177 account
.setUsername(accountRecord
.getUsername() != null && !accountRecord
.getUsername().isEmpty()
178 ? accountRecord
.getUsername()
180 if (accountRecord
.getUsernameLink() != null) {
181 final var usernameLink
= accountRecord
.getUsernameLink();
182 account
.setUsernameLink(new UsernameLinkComponents(usernameLink
.entropy
.toByteArray(),
183 UuidUtil
.parseOrThrow(usernameLink
.serverId
.toByteArray())));
184 account
.getConfigurationStore().setUsernameLinkColor(connection
, usernameLink
.color
.name());
187 if (accountRecord
.getProfileKey().isPresent()) {
188 ProfileKey profileKey
;
190 profileKey
= new ProfileKey(accountRecord
.getProfileKey().get());
191 } catch (InvalidInputException e
) {
192 logger
.debug("Received invalid profile key from storage");
195 if (profileKey
!= null) {
196 account
.setProfileKey(profileKey
);
197 final var avatarPath
= accountRecord
.getAvatarUrlPath().orElse(null);
198 jobExecutor
.enqueueJob(new DownloadProfileAvatarJob(avatarPath
));
202 final var profile
= account
.getRecipientStore().getProfile(connection
, account
.getSelfRecipientId());
203 final var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
204 builder
.withGivenName(accountRecord
.getGivenName().orElse(null));
205 builder
.withFamilyName(accountRecord
.getFamilyName().orElse(null));
206 account
.getRecipientStore().storeProfile(connection
, account
.getSelfRecipientId(), builder
.build());
207 account
.getRecipientStore()
208 .storeStorageRecord(connection
,
209 account
.getSelfRecipientId(),
210 accountRecord
.getId(),
211 accountRecord
.toProto().encode());
215 public int compare(SignalAccountRecord lhs
, SignalAccountRecord rhs
) {
219 private static boolean doProtosMatch(SignalAccountRecord merged
, SignalAccountRecord other
) {
220 return Arrays
.equals(merged
.toProto().encode(), other
.toProto().encode());