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() != null && !remote
.getUsername().isEmpty()
106 ? remote
.getUsername()
107 : local
.getUsername() != null && !local
.getUsername().isEmpty() ? local
.getUsername() : null;
108 final var usernameLink
= remote
.getUsernameLink() != null ? remote
.getUsernameLink() : local
.getUsernameLink();
110 final var mergedBuilder
= new SignalAccountRecord
.Builder(remote
.getId().getRaw(), unknownFields
).setGivenName(
112 .setFamilyName(familyName
)
113 .setAvatarUrlPath(avatarUrlPath
)
114 .setProfileKey(profileKey
)
115 .setNoteToSelfArchived(noteToSelfArchived
)
116 .setNoteToSelfForcedUnread(noteToSelfForcedUnread
)
117 .setReadReceiptsEnabled(readReceipts
)
118 .setTypingIndicatorsEnabled(typingIndicators
)
119 .setSealedSenderIndicatorsEnabled(sealedSenderIndicators
)
120 .setLinkPreviewsEnabled(linkPreviews
)
121 .setUnlistedPhoneNumber(unlisted
)
122 .setPhoneNumberSharingMode(phoneNumberSharingMode
)
123 .setUnlistedPhoneNumber(unlisted
)
124 .setPinnedConversations(pinnedConversations
)
125 .setPreferContactAvatars(preferContactAvatars
)
126 .setPayments(payments
.isEnabled(), payments
.getEntropy().orElse(null))
127 .setUniversalExpireTimer(universalExpireTimer
)
128 .setDefaultReactions(defaultReactions
)
129 .setSubscriber(subscriber
)
130 .setDisplayBadgesOnProfile(displayBadgesOnProfile
)
131 .setSubscriptionManuallyCancelled(subscriptionManuallyCancelled
)
132 .setKeepMutedChatsArchived(keepMutedChatsArchived
)
133 .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy
)
134 .setHasViewedOnboardingStory(hasViewedOnboardingStory
)
135 .setStoriesDisabled(storiesDisabled
)
136 .setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation
)
137 .setStoryViewReceiptsState(storyViewReceiptsState
)
138 .setUsername(username
)
139 .setUsernameLink(usernameLink
)
141 final var merged
= mergedBuilder
.build();
143 final var matchesRemote
= doProtosMatch(merged
, remote
);
148 final var matchesLocal
= doProtosMatch(merged
, local
);
153 return mergedBuilder
.setId(KeyUtils
.createRawStorageId()).build();
157 protected void insertLocal(SignalAccountRecord
record) {
158 throw new UnsupportedOperationException(
159 "We should always have a local AccountRecord, so we should never been inserting a new one.");
163 protected void updateLocal(StorageRecordUpdate
<SignalAccountRecord
> update
) throws SQLException
{
164 final var accountRecord
= update
.newRecord();
166 if (!accountRecord
.getE164().equals(account
.getNumber())) {
167 jobExecutor
.enqueueJob(new CheckWhoAmIJob());
170 account
.getConfigurationStore().setReadReceipts(connection
, accountRecord
.isReadReceiptsEnabled());
171 account
.getConfigurationStore().setTypingIndicators(connection
, accountRecord
.isTypingIndicatorsEnabled());
172 account
.getConfigurationStore()
173 .setUnidentifiedDeliveryIndicators(connection
, accountRecord
.isSealedSenderIndicatorsEnabled());
174 account
.getConfigurationStore().setLinkPreviews(connection
, accountRecord
.isLinkPreviewsEnabled());
175 account
.getConfigurationStore()
176 .setPhoneNumberSharingMode(connection
,
177 StorageSyncModels
.remoteToLocal(accountRecord
.getPhoneNumberSharingMode()));
178 account
.getConfigurationStore().setPhoneNumberUnlisted(connection
, accountRecord
.isPhoneNumberUnlisted());
180 account
.setUsername(accountRecord
.getUsername() != null && !accountRecord
.getUsername().isEmpty()
181 ? accountRecord
.getUsername()
183 if (accountRecord
.getUsernameLink() != null) {
184 final var usernameLink
= accountRecord
.getUsernameLink();
185 account
.setUsernameLink(new UsernameLinkComponents(usernameLink
.entropy
.toByteArray(),
186 UuidUtil
.parseOrThrow(usernameLink
.serverId
.toByteArray())));
187 account
.getConfigurationStore().setUsernameLinkColor(connection
, usernameLink
.color
.name());
190 if (accountRecord
.getProfileKey().isPresent()) {
191 ProfileKey profileKey
;
193 profileKey
= new ProfileKey(accountRecord
.getProfileKey().get());
194 } catch (InvalidInputException e
) {
195 logger
.debug("Received invalid profile key from storage");
198 if (profileKey
!= null) {
199 account
.setProfileKey(profileKey
);
200 final var avatarPath
= accountRecord
.getAvatarUrlPath().orElse(null);
201 jobExecutor
.enqueueJob(new DownloadProfileAvatarJob(avatarPath
));
205 final var profile
= account
.getRecipientStore().getProfile(connection
, account
.getSelfRecipientId());
206 final var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
207 builder
.withGivenName(accountRecord
.getGivenName().orElse(null));
208 builder
.withFamilyName(accountRecord
.getFamilyName().orElse(null));
209 account
.getRecipientStore().storeProfile(connection
, account
.getSelfRecipientId(), builder
.build());
210 account
.getRecipientStore()
211 .storeStorageRecord(connection
,
212 account
.getSelfRecipientId(),
213 accountRecord
.getId(),
214 accountRecord
.toProto().encode());
218 public int compare(SignalAccountRecord lhs
, SignalAccountRecord rhs
) {
222 private static boolean doProtosMatch(SignalAccountRecord merged
, SignalAccountRecord other
) {
223 return Arrays
.equals(merged
.toProto().encode(), other
.toProto().encode());