]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
Fix storage sync issues
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / SyncHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.api.Contact;
4 import org.asamk.signal.manager.api.GroupId;
5 import org.asamk.signal.manager.api.TrustLevel;
6 import org.asamk.signal.manager.storage.SignalAccount;
7 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
8 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
9 import org.asamk.signal.manager.storage.stickers.StickerPack;
10 import org.asamk.signal.manager.util.AttachmentUtils;
11 import org.asamk.signal.manager.util.IOUtils;
12 import org.asamk.signal.manager.util.MimeUtils;
13 import org.signal.libsignal.protocol.IdentityKey;
14 import org.slf4j.Logger;
15 import org.slf4j.LoggerFactory;
16 import org.whispersystems.signalservice.api.messages.SendMessageResult;
17 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
18 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
19 import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
20 import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
21 import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
22 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
23 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
24 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
25 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
26 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
27 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
28 import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
29 import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
30 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
31 import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
32 import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
33 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
34 import org.whispersystems.signalservice.internal.push.SyncMessage;
35
36 import java.io.FileInputStream;
37 import java.io.FileOutputStream;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.io.OutputStream;
41 import java.nio.file.Files;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Optional;
45 import java.util.stream.Collectors;
46 import java.util.stream.Stream;
47
48 public class SyncHelper {
49
50 private static final Logger logger = LoggerFactory.getLogger(SyncHelper.class);
51
52 private final Context context;
53 private final SignalAccount account;
54
55 public SyncHelper(final Context context) {
56 this.context = context;
57 this.account = context.getAccount();
58 }
59
60 public void requestAllSyncData() {
61 requestSyncData(SyncMessage.Request.Type.GROUPS);
62 requestSyncData(SyncMessage.Request.Type.CONTACTS);
63 requestSyncData(SyncMessage.Request.Type.BLOCKED);
64 requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
65 requestSyncKeys();
66 requestSyncPniIdentity();
67 }
68
69 public void requestSyncKeys() {
70 requestSyncData(SyncMessage.Request.Type.KEYS);
71 }
72
73 public void requestSyncPniIdentity() {
74 requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY);
75 }
76
77 public SendMessageResult sendSyncFetchProfileMessage() {
78 return context.getSendHelper()
79 .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
80 }
81
82 public void sendSyncFetchStorageMessage() {
83 context.getSendHelper()
84 .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST));
85 }
86
87 public void sendGroups() throws IOException {
88 var groupsFile = IOUtils.createTempFile();
89
90 try {
91 try (OutputStream fos = new FileOutputStream(groupsFile)) {
92 var out = new DeviceGroupsOutputStream(fos);
93 for (var record : account.getGroupStore().getGroups()) {
94 if (record instanceof GroupInfoV1 groupInfo) {
95 out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
96 Optional.ofNullable(groupInfo.name),
97 groupInfo.getMembers()
98 .stream()
99 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
100 .toList(),
101 context.getGroupHelper().createGroupAvatarAttachment(groupInfo.getGroupId()),
102 groupInfo.isMember(account.getSelfRecipientId()),
103 Optional.of(groupInfo.messageExpirationTime),
104 Optional.ofNullable(groupInfo.color),
105 groupInfo.blocked,
106 Optional.empty(),
107 groupInfo.archived));
108 }
109 }
110 }
111
112 if (groupsFile.exists() && groupsFile.length() > 0) {
113 try (var groupsFileStream = new FileInputStream(groupsFile)) {
114 var attachmentStream = SignalServiceAttachment.newStreamBuilder()
115 .withStream(groupsFileStream)
116 .withContentType(MimeUtils.OCTET_STREAM)
117 .withLength(groupsFile.length())
118 .build();
119
120 context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
121 }
122 }
123 } finally {
124 try {
125 Files.delete(groupsFile.toPath());
126 } catch (IOException e) {
127 logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage());
128 }
129 }
130 }
131
132 public void sendContacts() throws IOException {
133 var contactsFile = IOUtils.createTempFile();
134
135 try {
136 try (OutputStream fos = new FileOutputStream(contactsFile)) {
137 var out = new DeviceContactsOutputStream(fos);
138 for (var contactPair : account.getContactStore().getContacts()) {
139 final var recipientId = contactPair.first();
140 final var contact = contactPair.second();
141 final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
142
143 var currentIdentity = account.getIdentityKeyStore().getIdentityInfo(address.getServiceId());
144 VerifiedMessage verifiedMessage = null;
145 if (currentIdentity != null) {
146 verifiedMessage = new VerifiedMessage(address,
147 currentIdentity.getIdentityKey(),
148 currentIdentity.getTrustLevel().toVerifiedState(),
149 currentIdentity.getDateAddedTimestamp());
150 }
151
152 var profileKey = account.getProfileStore().getProfileKey(recipientId);
153 out.write(new DeviceContact(address,
154 Optional.ofNullable(contact.getName()),
155 createContactAvatarAttachment(new RecipientAddress(address)),
156 Optional.ofNullable(contact.color()),
157 Optional.ofNullable(verifiedMessage),
158 Optional.ofNullable(profileKey),
159 contact.isBlocked(),
160 Optional.of(contact.messageExpirationTime()),
161 Optional.empty(),
162 contact.isArchived()));
163 }
164
165 if (account.getProfileKey() != null) {
166 // Send our own profile key as well
167 out.write(new DeviceContact(account.getSelfAddress(),
168 Optional.empty(),
169 Optional.empty(),
170 Optional.empty(),
171 Optional.empty(),
172 Optional.of(account.getProfileKey()),
173 false,
174 Optional.empty(),
175 Optional.empty(),
176 false));
177 }
178 }
179
180 if (contactsFile.exists() && contactsFile.length() > 0) {
181 try (var contactsFileStream = new FileInputStream(contactsFile)) {
182 var attachmentStream = SignalServiceAttachment.newStreamBuilder()
183 .withStream(contactsFileStream)
184 .withContentType(MimeUtils.OCTET_STREAM)
185 .withLength(contactsFile.length())
186 .build();
187
188 context.getSendHelper()
189 .sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream,
190 true)));
191 }
192 }
193 } finally {
194 try {
195 Files.delete(contactsFile.toPath());
196 } catch (IOException e) {
197 logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage());
198 }
199 }
200 }
201
202 public SendMessageResult sendBlockedList() {
203 var addresses = new ArrayList<SignalServiceAddress>();
204 for (var record : account.getContactStore().getContacts()) {
205 if (record.second().isBlocked()) {
206 addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
207 }
208 }
209 var groupIds = new ArrayList<byte[]>();
210 for (var record : account.getGroupStore().getGroups()) {
211 if (record.isBlocked()) {
212 groupIds.add(record.getGroupId().serialize());
213 }
214 }
215 return context.getSendHelper()
216 .sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
217 }
218
219 public SendMessageResult sendVerifiedMessage(
220 SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
221 ) {
222 var verifiedMessage = new VerifiedMessage(destination,
223 identityKey,
224 trustLevel.toVerifiedState(),
225 System.currentTimeMillis());
226 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
227 }
228
229 public SendMessageResult sendKeysMessage() {
230 var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()),
231 Optional.ofNullable(account.getOrCreatePinMasterKey()));
232 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
233 }
234
235 public SendMessageResult sendStickerOperationsMessage(
236 List<StickerPack> installStickers, List<StickerPack> removeStickers
237 ) {
238 var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
239 var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
240 var stickerMessages = Stream.concat(installStickerMessages, removeStickerMessages).toList();
241 return context.getSendHelper()
242 .sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(stickerMessages));
243 }
244
245 private static StickerPackOperationMessage getStickerPackOperationMessage(
246 final StickerPack s, final boolean installed
247 ) {
248 return new StickerPackOperationMessage(s.packId().serialize(),
249 s.packKey(),
250 installed ? StickerPackOperationMessage.Type.INSTALL : StickerPackOperationMessage.Type.REMOVE);
251 }
252
253 public SendMessageResult sendConfigurationMessage() {
254 final var config = account.getConfigurationStore();
255 var configurationMessage = new ConfigurationMessage(Optional.ofNullable(config.getReadReceipts()),
256 Optional.ofNullable(config.getUnidentifiedDeliveryIndicators()),
257 Optional.ofNullable(config.getTypingIndicators()),
258 Optional.ofNullable(config.getLinkPreviews()));
259 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
260 }
261
262 public void handleSyncDeviceGroups(final InputStream input) {
263 final var s = new DeviceGroupsInputStream(input);
264 DeviceGroup g;
265 while (true) {
266 try {
267 g = s.read();
268 } catch (IOException e) {
269 logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
270 continue;
271 }
272 if (g == null) {
273 break;
274 }
275 var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
276 if (syncGroup != null) {
277 if (g.getName().isPresent()) {
278 syncGroup.name = g.getName().get();
279 }
280 syncGroup.addMembers(g.getMembers()
281 .stream()
282 .map(account.getRecipientResolver()::resolveRecipient)
283 .collect(Collectors.toSet()));
284 if (!g.isActive()) {
285 syncGroup.removeMember(account.getSelfRecipientId());
286 } else {
287 // Add ourself to the member set as it's marked as active
288 syncGroup.addMembers(List.of(account.getSelfRecipientId()));
289 }
290 syncGroup.blocked = g.isBlocked();
291 if (g.getColor().isPresent()) {
292 syncGroup.color = g.getColor().get();
293 }
294
295 if (g.getAvatar().isPresent()) {
296 context.getGroupHelper().downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
297 }
298 syncGroup.archived = g.isArchived();
299 account.getGroupStore().updateGroup(syncGroup);
300 }
301 }
302 }
303
304 public void handleSyncDeviceContacts(final InputStream input) throws IOException {
305 final var s = new DeviceContactsInputStream(input);
306 DeviceContact c;
307 while (true) {
308 try {
309 c = s.read();
310 } catch (IOException e) {
311 if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) {
312 logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
313 continue;
314 } else {
315 throw e;
316 }
317 }
318 if (c == null) {
319 break;
320 }
321 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
322 account.setProfileKey(c.getProfileKey().get());
323 }
324 final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(c.getAddress());
325 var contact = account.getContactStore().getContact(recipientId);
326 final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
327 if (c.getName().isPresent() && (
328 contact == null || (
329 contact.givenName() == null
330 && contact.familyName() == null
331 )
332 )) {
333 builder.withGivenName(c.getName().get());
334 builder.withFamilyName(null);
335 }
336 if (c.getColor().isPresent()) {
337 builder.withColor(c.getColor().get());
338 }
339 if (c.getProfileKey().isPresent()) {
340 account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
341 }
342 if (c.getVerified().isPresent()) {
343 final var verifiedMessage = c.getVerified().get();
344 account.getIdentityKeyStore()
345 .setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
346 verifiedMessage.getIdentityKey(),
347 TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
348 }
349 if (c.getExpirationTimer().isPresent()) {
350 builder.withMessageExpirationTime(c.getExpirationTimer().get());
351 }
352 builder.withIsBlocked(c.isBlocked());
353 builder.withIsArchived(c.isArchived());
354 account.getContactStore().storeContact(recipientId, builder.build());
355
356 if (c.getAvatar().isPresent()) {
357 downloadContactAvatar(c.getAvatar().get(), new RecipientAddress(c.getAddress()));
358 }
359 }
360 }
361
362 private SendMessageResult requestSyncData(final SyncMessage.Request.Type type) {
363 var r = new SyncMessage.Request.Builder().type(type).build();
364 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
365 return context.getSendHelper().sendSyncMessage(message);
366 }
367
368 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(RecipientAddress address) throws IOException {
369 final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address);
370 if (streamDetails == null) {
371 return Optional.empty();
372 }
373
374 return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty()));
375 }
376
377 private void downloadContactAvatar(SignalServiceAttachment avatar, RecipientAddress address) {
378 try {
379 context.getAvatarStore()
380 .storeContactAvatar(address,
381 outputStream -> context.getAttachmentHelper().retrieveAttachment(avatar, outputStream));
382 } catch (IOException e) {
383 logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
384 }
385 }
386 }