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