]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java
21c0d97a7114fae2c1d4408dc63713afd405ddb0
[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.DownloadProfileAvatarJob;
6 import org.asamk.signal.manager.storage.SignalAccount;
7 import org.asamk.signal.manager.util.KeyUtils;
8 import org.signal.libsignal.zkgroup.InvalidInputException;
9 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
10 import org.slf4j.Logger;
11 import org.slf4j.LoggerFactory;
12 import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
13 import org.whispersystems.signalservice.api.storage.IAPSubscriptionId;
14 import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
15 import org.whispersystems.signalservice.api.storage.StorageId;
16 import org.whispersystems.signalservice.api.util.UuidUtil;
17 import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
18 import org.whispersystems.signalservice.internal.storage.protos.OptionalBool;
19
20 import java.sql.Connection;
21 import java.sql.SQLException;
22 import java.util.Arrays;
23 import java.util.Optional;
24
25 import okio.ByteString;
26
27 import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
28 import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetBackupsSubscriber;
29 import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetPayments;
30 import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetSubscriber;
31
32 /**
33 * Processes {@link SignalAccountRecord}s.
34 */
35 public class AccountRecordProcessor extends DefaultStorageRecordProcessor<SignalAccountRecord> {
36
37 private static final Logger logger = LoggerFactory.getLogger(AccountRecordProcessor.class);
38 private final SignalAccountRecord localAccountRecord;
39 private final SignalAccount account;
40 private final Connection connection;
41 private final JobExecutor jobExecutor;
42
43 public AccountRecordProcessor(
44 SignalAccount account,
45 Connection connection,
46 final JobExecutor jobExecutor
47 ) throws SQLException {
48 this.account = account;
49 this.connection = connection;
50 this.jobExecutor = jobExecutor;
51 final var selfRecipientId = account.getSelfRecipientId();
52 final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId);
53 final var storageId = account.getRecipientStore().getSelfStorageId(connection);
54 this.localAccountRecord = new SignalAccountRecord(storageId,
55 StorageSyncModels.localToRemoteRecord(connection,
56 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 .avatarColor(remote.avatarColor);
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 account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts);
182 account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators);
183 account.getConfigurationStore()
184 .setUnidentifiedDeliveryIndicators(connection, accountProto.sealedSenderIndicators);
185 account.getConfigurationStore().setLinkPreviews(connection, accountProto.linkPreviews);
186 account.getConfigurationStore()
187 .setPhoneNumberSharingMode(connection,
188 StorageSyncModels.remoteToLocal(accountProto.phoneNumberSharingMode));
189 account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountProto.unlistedPhoneNumber);
190
191 account.setUsername(!accountProto.username.isEmpty() ? accountProto.username : null);
192 if (accountProto.usernameLink != null) {
193 final var usernameLink = accountProto.usernameLink;
194 account.setUsernameLink(new UsernameLinkComponents(usernameLink.entropy.toByteArray(),
195 UuidUtil.parseOrThrow(usernameLink.serverId.toByteArray())));
196 account.getConfigurationStore().setUsernameLinkColor(connection, usernameLink.color.name());
197 }
198
199 if (accountProto.profileKey.size() > 0) {
200 ProfileKey profileKey;
201 try {
202 profileKey = new ProfileKey(accountProto.profileKey.toByteArray());
203 } catch (InvalidInputException e) {
204 logger.debug("Received invalid profile key from storage");
205 profileKey = null;
206 }
207 if (profileKey != null) {
208 account.setProfileKey(profileKey);
209 final var avatarPath = accountProto.avatarUrlPath.isEmpty() ? null : accountProto.avatarUrlPath;
210 jobExecutor.enqueueJob(new DownloadProfileAvatarJob(avatarPath));
211 }
212 }
213
214 final var profile = account.getRecipientStore().getProfile(connection, account.getSelfRecipientId());
215 final var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
216 builder.withGivenName(accountProto.givenName);
217 builder.withFamilyName(accountProto.familyName);
218 account.getRecipientStore().storeProfile(connection, account.getSelfRecipientId(), builder.build());
219 account.getRecipientStore()
220 .storeStorageRecord(connection,
221 account.getSelfRecipientId(),
222 accountRecord.getId(),
223 accountProto.encode());
224 }
225
226 @Override
227 public int compare(SignalAccountRecord lhs, SignalAccountRecord rhs) {
228 return 0;
229 }
230
231 private static boolean doProtosMatch(AccountRecord merged, AccountRecord other) {
232 return Arrays.equals(merged.encode(), other.encode());
233 }
234 }