]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java
656297bf7fe6653869d1298e6c9484ebd54d3db5
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / syncStorage / AccountRecordProcessor.java
1 package org.asamk.signal.manager.syncStorage;
2
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.IAPSubscriptionId;
15 import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
16 import org.whispersystems.signalservice.api.storage.StorageId;
17 import org.whispersystems.signalservice.api.util.UuidUtil;
18 import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
19 import org.whispersystems.signalservice.internal.storage.protos.OptionalBool;
20
21 import java.sql.Connection;
22 import java.sql.SQLException;
23 import java.util.Arrays;
24 import java.util.Optional;
25
26 import okio.ByteString;
27
28 import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
29 import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetBackupsSubscriber;
30 import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetPayments;
31 import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetSubscriber;
32
33 /**
34 * Processes {@link SignalAccountRecord}s.
35 */
36 public class AccountRecordProcessor extends DefaultStorageRecordProcessor<SignalAccountRecord> {
37
38 private static final Logger logger = LoggerFactory.getLogger(AccountRecordProcessor.class);
39 private final SignalAccountRecord localAccountRecord;
40 private final SignalAccount account;
41 private final Connection connection;
42 private final JobExecutor jobExecutor;
43
44 public AccountRecordProcessor(
45 SignalAccount account,
46 Connection connection,
47 final JobExecutor jobExecutor
48 ) throws SQLException {
49 this.account = account;
50 this.connection = connection;
51 this.jobExecutor = jobExecutor;
52 final var selfRecipientId = account.getSelfRecipientId();
53 final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId);
54 final var storageId = account.getRecipientStore().getSelfStorageId(connection);
55 this.localAccountRecord = new SignalAccountRecord(storageId,
56 StorageSyncModels.localToRemoteRecord(connection,
57 account.getConfigurationStore(),
58 recipient,
59 account.getUsernameLink()));
60 }
61
62 @Override
63 protected boolean isInvalid(SignalAccountRecord remote) {
64 return false;
65 }
66
67 @Override
68 protected Optional<SignalAccountRecord> getMatching(SignalAccountRecord record) {
69 return Optional.of(localAccountRecord);
70 }
71
72 @Override
73 protected SignalAccountRecord merge(SignalAccountRecord remoteRecord, SignalAccountRecord localRecord) {
74 final var remote = remoteRecord.getProto();
75 final var local = localRecord.getProto();
76 String givenName;
77 String familyName;
78 if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
79 givenName = remote.givenName;
80 familyName = remote.familyName;
81 } else {
82 givenName = local.givenName;
83 familyName = local.familyName;
84 }
85
86 final var payments = remote.payments != null && remote.payments.entropy.size() > 0
87 ? remote.payments
88 : local.payments;
89
90 final ByteString donationSubscriberId;
91 final String donationSubscriberCurrencyCode;
92
93 if (remote.subscriberId.size() > 0) {
94 donationSubscriberId = remote.subscriberId;
95 donationSubscriberCurrencyCode = remote.subscriberCurrencyCode;
96 } else {
97 donationSubscriberId = local.subscriberId;
98 donationSubscriberCurrencyCode = local.subscriberCurrencyCode;
99 }
100
101 final ByteString backupsSubscriberId;
102 final IAPSubscriptionId backupsPurchaseToken;
103
104 final var remoteBackupSubscriberData = remote.backupSubscriberData;
105 if (remoteBackupSubscriberData != null && remoteBackupSubscriberData.subscriberId.size() > 0) {
106 backupsSubscriberId = remoteBackupSubscriberData.subscriberId;
107 backupsPurchaseToken = IAPSubscriptionId.Companion.from(remoteBackupSubscriberData);
108 } else {
109 backupsSubscriberId = local.backupSubscriberData != null
110 ? local.backupSubscriberData.subscriberId
111 : ByteString.EMPTY;
112 backupsPurchaseToken = IAPSubscriptionId.Companion.from(local.backupSubscriberData);
113 }
114
115 final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
116 .givenName(givenName)
117 .familyName(familyName)
118 .avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))
119 .profileKey(firstNonEmpty(remote.profileKey, local.profileKey))
120 .noteToSelfArchived(remote.noteToSelfArchived)
121 .noteToSelfMarkedUnread(remote.noteToSelfMarkedUnread)
122 .readReceipts(remote.readReceipts)
123 .typingIndicators(remote.typingIndicators)
124 .sealedSenderIndicators(remote.sealedSenderIndicators)
125 .linkPreviews(remote.linkPreviews)
126 .unlistedPhoneNumber(remote.unlistedPhoneNumber)
127 .phoneNumberSharingMode(remote.phoneNumberSharingMode)
128 .pinnedConversations(remote.pinnedConversations)
129 .preferContactAvatars(remote.preferContactAvatars)
130 .universalExpireTimer(remote.universalExpireTimer)
131 .preferredReactionEmoji(firstNonEmpty(remote.preferredReactionEmoji, local.preferredReactionEmoji))
132 .subscriberId(firstNonEmpty(remote.subscriberId, local.subscriberId))
133 .subscriberCurrencyCode(firstNonEmpty(remote.subscriberCurrencyCode, local.subscriberCurrencyCode))
134 .displayBadgesOnProfile(remote.displayBadgesOnProfile)
135 .subscriptionManuallyCancelled(remote.subscriptionManuallyCancelled)
136 .keepMutedChatsArchived(remote.keepMutedChatsArchived)
137 .hasSetMyStoriesPrivacy(remote.hasSetMyStoriesPrivacy)
138 .hasViewedOnboardingStory(remote.hasViewedOnboardingStory || local.hasViewedOnboardingStory)
139 .storiesDisabled(remote.storiesDisabled)
140 .hasSeenGroupStoryEducationSheet(remote.hasSeenGroupStoryEducationSheet
141 || local.hasSeenGroupStoryEducationSheet)
142 .hasCompletedUsernameOnboarding(remote.hasCompletedUsernameOnboarding
143 || local.hasCompletedUsernameOnboarding)
144 .storyViewReceiptsEnabled(remote.storyViewReceiptsEnabled == OptionalBool.UNSET
145 ? local.storyViewReceiptsEnabled
146 : remote.storyViewReceiptsEnabled)
147 .username(remote.username)
148 .usernameLink(remote.usernameLink)
149 .e164(account.isPrimaryDevice() ? local.e164 : remote.e164);
150 safeSetPayments(mergedBuilder,
151 payments != null && payments.enabled,
152 payments == null ? null : payments.entropy.toByteArray());
153 safeSetSubscriber(mergedBuilder, donationSubscriberId, donationSubscriberCurrencyCode);
154 safeSetBackupsSubscriber(mergedBuilder, backupsSubscriberId, backupsPurchaseToken);
155
156 final var merged = mergedBuilder.build();
157
158 final var matchesRemote = doProtosMatch(merged, remote);
159 if (matchesRemote) {
160 return remoteRecord;
161 }
162
163 final var matchesLocal = doProtosMatch(merged, local);
164 if (matchesLocal) {
165 return localRecord;
166 }
167
168 return new SignalAccountRecord(StorageId.forAccount(KeyUtils.createRawStorageId()), mergedBuilder.build());
169 }
170
171 @Override
172 protected void insertLocal(SignalAccountRecord record) {
173 throw new UnsupportedOperationException(
174 "We should always have a local AccountRecord, so we should never been inserting a new one.");
175 }
176
177 @Override
178 protected void updateLocal(StorageRecordUpdate<SignalAccountRecord> update) throws SQLException {
179 final var accountRecord = update.newRecord();
180 final var accountProto = accountRecord.getProto();
181
182 if (!accountProto.e164.equals(account.getNumber())) {
183 jobExecutor.enqueueJob(new CheckWhoAmIJob());
184 }
185
186 account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts);
187 account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators);
188 account.getConfigurationStore()
189 .setUnidentifiedDeliveryIndicators(connection, accountProto.sealedSenderIndicators);
190 account.getConfigurationStore().setLinkPreviews(connection, accountProto.linkPreviews);
191 account.getConfigurationStore()
192 .setPhoneNumberSharingMode(connection,
193 StorageSyncModels.remoteToLocal(accountProto.phoneNumberSharingMode));
194 account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountProto.unlistedPhoneNumber);
195
196 account.setUsername(!accountProto.username.isEmpty() ? accountProto.username : null);
197 if (accountProto.usernameLink != null) {
198 final var usernameLink = accountProto.usernameLink;
199 account.setUsernameLink(new UsernameLinkComponents(usernameLink.entropy.toByteArray(),
200 UuidUtil.parseOrThrow(usernameLink.serverId.toByteArray())));
201 account.getConfigurationStore().setUsernameLinkColor(connection, usernameLink.color.name());
202 }
203
204 if (accountProto.profileKey.size() > 0) {
205 ProfileKey profileKey;
206 try {
207 profileKey = new ProfileKey(accountProto.profileKey.toByteArray());
208 } catch (InvalidInputException e) {
209 logger.debug("Received invalid profile key from storage");
210 profileKey = null;
211 }
212 if (profileKey != null) {
213 account.setProfileKey(profileKey);
214 final var avatarPath = accountProto.avatarUrlPath.isEmpty() ? null : accountProto.avatarUrlPath;
215 jobExecutor.enqueueJob(new DownloadProfileAvatarJob(avatarPath));
216 }
217 }
218
219 final var profile = account.getRecipientStore().getProfile(connection, account.getSelfRecipientId());
220 final var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
221 builder.withGivenName(accountProto.givenName);
222 builder.withFamilyName(accountProto.familyName);
223 account.getRecipientStore().storeProfile(connection, account.getSelfRecipientId(), builder.build());
224 account.getRecipientStore()
225 .storeStorageRecord(connection,
226 account.getSelfRecipientId(),
227 accountRecord.getId(),
228 accountProto.encode());
229 }
230
231 @Override
232 public int compare(SignalAccountRecord lhs, SignalAccountRecord rhs) {
233 return 0;
234 }
235
236 private static boolean doProtosMatch(AccountRecord merged, AccountRecord other) {
237 return Arrays.equals(merged.encode(), other.encode());
238 }
239 }