]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
Fix truncating cdsi table
[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.MessageEnvelope.Sync.MessageRequestResponse;
6 import org.asamk.signal.manager.api.TrustLevel;
7 import org.asamk.signal.manager.storage.SignalAccount;
8 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
9 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
10 import org.asamk.signal.manager.storage.recipients.RecipientId;
11 import org.asamk.signal.manager.storage.stickers.StickerPack;
12 import org.asamk.signal.manager.util.IOUtils;
13 import org.asamk.signal.manager.util.MimeUtils;
14 import org.jetbrains.annotations.NotNull;
15 import org.signal.libsignal.protocol.IdentityKey;
16 import org.slf4j.Logger;
17 import org.slf4j.LoggerFactory;
18 import org.whispersystems.signalservice.api.messages.SendMessageResult;
19 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
20 import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
21 import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
22 import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
23 import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
24 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
25 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactAvatar;
26 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
27 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
28 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
29 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
30 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
31 import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
32 import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
33 import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
34 import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
35 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
36 import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
37 import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
38 import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
39 import org.whispersystems.signalservice.api.push.ServiceId;
40 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
41 import org.whispersystems.signalservice.internal.push.SyncMessage;
42 import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
43
44 import java.io.FileInputStream;
45 import java.io.FileOutputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
49 import java.nio.file.Files;
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.Optional;
53 import java.util.stream.Collectors;
54 import java.util.stream.Stream;
55
56 public class SyncHelper {
57
58 private static final Logger logger = LoggerFactory.getLogger(SyncHelper.class);
59
60 private final Context context;
61 private final SignalAccount account;
62
63 public SyncHelper(final Context context) {
64 this.context = context;
65 this.account = context.getAccount();
66 }
67
68 public void requestAllSyncData() {
69 requestSyncData(SyncMessage.Request.Type.GROUPS);
70 requestSyncData(SyncMessage.Request.Type.CONTACTS);
71 requestSyncData(SyncMessage.Request.Type.BLOCKED);
72 requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
73 requestSyncKeys();
74 requestSyncPniIdentity();
75 }
76
77 public void requestSyncKeys() {
78 requestSyncData(SyncMessage.Request.Type.KEYS);
79 }
80
81 public void requestSyncPniIdentity() {
82 requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY);
83 }
84
85 public SendMessageResult sendSyncFetchProfileMessage() {
86 return context.getSendHelper()
87 .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
88 }
89
90 public void sendSyncFetchStorageMessage() {
91 context.getSendHelper()
92 .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST));
93 }
94
95 public void sendSyncReceiptMessage(ServiceId sender, SignalServiceReceiptMessage receiptMessage) {
96 if (receiptMessage.isReadReceipt()) {
97 final var readMessages = receiptMessage.getTimestamps()
98 .stream()
99 .map(t -> new ReadMessage(sender, t))
100 .toList();
101 context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages));
102 } else if (receiptMessage.isViewedReceipt()) {
103 final var viewedMessages = receiptMessage.getTimestamps()
104 .stream()
105 .map(t -> new ViewedMessage(sender, t))
106 .toList();
107 context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages));
108 }
109 }
110
111 public void sendGroups() throws IOException {
112 var groupsFile = IOUtils.createTempFile();
113
114 try {
115 try (OutputStream fos = new FileOutputStream(groupsFile)) {
116 var out = new DeviceGroupsOutputStream(fos);
117 for (var record : account.getGroupStore().getGroups()) {
118 if (record instanceof GroupInfoV1 groupInfo) {
119 out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
120 Optional.ofNullable(groupInfo.name),
121 groupInfo.getMembers()
122 .stream()
123 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
124 .toList(),
125 context.getGroupHelper().createGroupAvatarAttachment(groupInfo.getGroupId()),
126 groupInfo.isMember(account.getSelfRecipientId()),
127 Optional.of(groupInfo.messageExpirationTime),
128 Optional.ofNullable(groupInfo.color),
129 groupInfo.blocked,
130 Optional.empty(),
131 groupInfo.archived));
132 }
133 }
134 }
135
136 if (groupsFile.exists() && groupsFile.length() > 0) {
137 try (var groupsFileStream = new FileInputStream(groupsFile)) {
138 final var uploadSpec = context.getDependencies()
139 .getMessageSender()
140 .getResumableUploadSpec()
141 .toProto();
142 var attachmentStream = SignalServiceAttachment.newStreamBuilder()
143 .withStream(groupsFileStream)
144 .withContentType(MimeUtils.OCTET_STREAM)
145 .withLength(groupsFile.length())
146 .withResumableUploadSpec(ResumableUploadSpec.from(uploadSpec))
147 .build();
148
149 context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
150 }
151 }
152 } finally {
153 try {
154 Files.delete(groupsFile.toPath());
155 } catch (IOException e) {
156 logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage());
157 }
158 }
159 }
160
161 public void sendContacts() throws IOException {
162 var contactsFile = IOUtils.createTempFile();
163
164 try {
165 try (OutputStream fos = new FileOutputStream(contactsFile)) {
166 var out = new DeviceContactsOutputStream(fos);
167 for (var contactPair : account.getContactStore().getContacts()) {
168 final var recipientId = contactPair.first();
169 final var contact = contactPair.second();
170 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
171
172 final var deviceContact = getDeviceContact(address, recipientId, contact);
173 out.write(deviceContact);
174 deviceContact.getAvatar().ifPresent(a -> {
175 try {
176 a.getInputStream().close();
177 } catch (IOException ignored) {
178 }
179 });
180 }
181
182 if (account.getProfileKey() != null) {
183 // Send our own profile key as well
184 final var address = account.getSelfRecipientAddress();
185 final var recipientId = account.getSelfRecipientId();
186 final var contact = account.getContactStore().getContact(recipientId);
187 final var deviceContact = getDeviceContact(address, recipientId, contact);
188 out.write(deviceContact);
189 deviceContact.getAvatar().ifPresent(a -> {
190 try {
191 a.getInputStream().close();
192 } catch (IOException ignored) {
193 }
194 });
195 }
196 }
197
198 if (contactsFile.exists() && contactsFile.length() > 0) {
199 try (var contactsFileStream = new FileInputStream(contactsFile)) {
200 final var uploadSpec = context.getDependencies()
201 .getMessageSender()
202 .getResumableUploadSpec()
203 .toProto();
204 var attachmentStream = SignalServiceAttachment.newStreamBuilder()
205 .withStream(contactsFileStream)
206 .withContentType(MimeUtils.OCTET_STREAM)
207 .withLength(contactsFile.length())
208 .withResumableUploadSpec(ResumableUploadSpec.from(uploadSpec))
209 .build();
210
211 context.getSendHelper()
212 .sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream,
213 true)));
214 }
215 }
216 } finally {
217 try {
218 Files.delete(contactsFile.toPath());
219 } catch (IOException e) {
220 logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage());
221 }
222 }
223 }
224
225 @NotNull
226 private DeviceContact getDeviceContact(
227 final RecipientAddress address, final RecipientId recipientId, final Contact contact
228 ) throws IOException {
229 var currentIdentity = address.serviceId().isEmpty()
230 ? null
231 : account.getIdentityKeyStore().getIdentityInfo(address.serviceId().get());
232 VerifiedMessage verifiedMessage = null;
233 if (currentIdentity != null) {
234 verifiedMessage = new VerifiedMessage(address.toSignalServiceAddress(),
235 currentIdentity.getIdentityKey(),
236 currentIdentity.getTrustLevel().toVerifiedState(),
237 currentIdentity.getDateAddedTimestamp());
238 }
239
240 var profileKey = account.getProfileStore().getProfileKey(recipientId);
241 return new DeviceContact(address.aci(),
242 address.number(),
243 Optional.ofNullable(contact == null ? null : contact.getName()),
244 createContactAvatarAttachment(address),
245 Optional.ofNullable(contact == null ? null : contact.color()),
246 Optional.ofNullable(verifiedMessage),
247 Optional.ofNullable(profileKey),
248 Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()),
249 Optional.empty(),
250 Optional.empty(),
251 contact != null && contact.isArchived());
252 }
253
254 public SendMessageResult sendBlockedList() {
255 var addresses = new ArrayList<SignalServiceAddress>();
256 for (var record : account.getContactStore().getContacts()) {
257 if (record.second().isBlocked()) {
258 addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
259 }
260 }
261 var groupIds = new ArrayList<byte[]>();
262 for (var record : account.getGroupStore().getGroups()) {
263 if (record.isBlocked()) {
264 groupIds.add(record.getGroupId().serialize());
265 }
266 }
267 return context.getSendHelper()
268 .sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
269 }
270
271 public SendMessageResult sendVerifiedMessage(
272 SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
273 ) {
274 var verifiedMessage = new VerifiedMessage(destination,
275 identityKey,
276 trustLevel.toVerifiedState(),
277 System.currentTimeMillis());
278 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
279 }
280
281 public SendMessageResult sendKeysMessage() {
282 var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()),
283 Optional.ofNullable(account.getOrCreatePinMasterKey()));
284 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
285 }
286
287 public SendMessageResult sendStickerOperationsMessage(
288 List<StickerPack> installStickers, List<StickerPack> removeStickers
289 ) {
290 var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
291 var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
292 var stickerMessages = Stream.concat(installStickerMessages, removeStickerMessages).toList();
293 return context.getSendHelper()
294 .sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(stickerMessages));
295 }
296
297 private static StickerPackOperationMessage getStickerPackOperationMessage(
298 final StickerPack s, final boolean installed
299 ) {
300 return new StickerPackOperationMessage(s.packId().serialize(),
301 s.packKey(),
302 installed ? StickerPackOperationMessage.Type.INSTALL : StickerPackOperationMessage.Type.REMOVE);
303 }
304
305 public SendMessageResult sendConfigurationMessage() {
306 final var config = account.getConfigurationStore();
307 var configurationMessage = new ConfigurationMessage(Optional.ofNullable(config.getReadReceipts()),
308 Optional.ofNullable(config.getUnidentifiedDeliveryIndicators()),
309 Optional.ofNullable(config.getTypingIndicators()),
310 Optional.ofNullable(config.getLinkPreviews()));
311 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
312 }
313
314 public void handleSyncDeviceGroups(final InputStream input) {
315 final var s = new DeviceGroupsInputStream(input);
316 DeviceGroup g;
317 while (true) {
318 try {
319 g = s.read();
320 } catch (IOException e) {
321 logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
322 continue;
323 }
324 if (g == null) {
325 break;
326 }
327 var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
328 if (syncGroup != null) {
329 if (g.getName().isPresent()) {
330 syncGroup.name = g.getName().get();
331 }
332 syncGroup.addMembers(g.getMembers()
333 .stream()
334 .map(account.getRecipientResolver()::resolveRecipient)
335 .collect(Collectors.toSet()));
336 if (!g.isActive()) {
337 syncGroup.removeMember(account.getSelfRecipientId());
338 } else {
339 // Add ourself to the member set as it's marked as active
340 syncGroup.addMembers(List.of(account.getSelfRecipientId()));
341 }
342 syncGroup.blocked = g.isBlocked();
343 if (g.getColor().isPresent()) {
344 syncGroup.color = g.getColor().get();
345 }
346
347 if (g.getAvatar().isPresent()) {
348 context.getGroupHelper().downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
349 }
350 syncGroup.archived = g.isArchived();
351 account.getGroupStore().updateGroup(syncGroup);
352 }
353 }
354 }
355
356 public void handleSyncDeviceContacts(final InputStream input) throws IOException {
357 final var s = new DeviceContactsInputStream(input);
358 DeviceContact c;
359 while (true) {
360 try {
361 c = s.read();
362 } catch (IOException e) {
363 if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) {
364 logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
365 continue;
366 } else {
367 throw e;
368 }
369 }
370 if (c == null || (c.getAci().isEmpty() && c.getE164().isEmpty())) {
371 break;
372 }
373 final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty());
374 if (address.matches(account.getSelfRecipientAddress()) && c.getProfileKey().isPresent()) {
375 account.setProfileKey(c.getProfileKey().get());
376 }
377 final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address);
378 var contact = account.getContactStore().getContact(recipientId);
379 final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
380 if (c.getName().isPresent() && (
381 contact == null || (
382 contact.givenName() == null && contact.familyName() == null
383 )
384 )) {
385 builder.withGivenName(c.getName().get());
386 builder.withFamilyName(null);
387 }
388 if (c.getColor().isPresent()) {
389 builder.withColor(c.getColor().get());
390 }
391 if (c.getProfileKey().isPresent()) {
392 account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
393 }
394 if (c.getVerified().isPresent()) {
395 final var verifiedMessage = c.getVerified().get();
396 account.getIdentityKeyStore()
397 .setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
398 verifiedMessage.getIdentityKey(),
399 TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
400 }
401 if (c.getExpirationTimer().isPresent()) {
402 builder.withMessageExpirationTime(c.getExpirationTimer().get());
403 }
404 builder.withIsArchived(c.isArchived());
405 account.getContactStore().storeContact(recipientId, builder.build());
406
407 if (c.getAvatar().isPresent()) {
408 storeContactAvatar(c.getAvatar().get(), address);
409 }
410 }
411 }
412
413 public SendMessageResult sendMessageRequestResponse(
414 final MessageRequestResponse.Type type, final GroupId groupId
415 ) {
416 final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type));
417 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
418 }
419
420 public SendMessageResult sendMessageRequestResponse(
421 final MessageRequestResponse.Type type, final RecipientId recipientId
422 ) {
423 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
424 if (address.serviceId().isEmpty()) {
425 return null;
426 }
427 context.getContactHelper()
428 .setContactProfileSharing(recipientId,
429 type == MessageRequestResponse.Type.ACCEPT
430 || type == MessageRequestResponse.Type.UNBLOCK_AND_ACCEPT);
431 final var response = MessageRequestResponseMessage.forIndividual(address.serviceId().get(),
432 localToRemoteType(type));
433 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
434 }
435
436 private SendMessageResult requestSyncData(final SyncMessage.Request.Type type) {
437 var r = new SyncMessage.Request.Builder().type(type).build();
438 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
439 return context.getSendHelper().sendSyncMessage(message);
440 }
441
442 private Optional<DeviceContactAvatar> createContactAvatarAttachment(RecipientAddress address) throws IOException {
443 final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address);
444 if (streamDetails == null) {
445 return Optional.empty();
446 }
447
448 return Optional.of(new DeviceContactAvatar(streamDetails.getStream(),
449 streamDetails.getLength(),
450 streamDetails.getContentType()));
451 }
452
453 private void storeContactAvatar(DeviceContactAvatar avatar, RecipientAddress address) {
454 try {
455 context.getAvatarStore()
456 .storeContactAvatar(address,
457 outputStream -> IOUtils.copyStream(avatar.getInputStream(), outputStream));
458 } catch (IOException e) {
459 logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
460 }
461 }
462
463 private MessageRequestResponseMessage.Type localToRemoteType(final MessageRequestResponse.Type type) {
464 return switch (type) {
465 case UNKNOWN -> MessageRequestResponseMessage.Type.UNKNOWN;
466 case ACCEPT -> MessageRequestResponseMessage.Type.ACCEPT;
467 case DELETE -> MessageRequestResponseMessage.Type.DELETE;
468 case BLOCK -> MessageRequestResponseMessage.Type.BLOCK;
469 case BLOCK_AND_DELETE -> MessageRequestResponseMessage.Type.BLOCK_AND_DELETE;
470 case UNBLOCK_AND_ACCEPT -> MessageRequestResponseMessage.Type.UNBLOCK_AND_ACCEPT;
471 case SPAM -> MessageRequestResponseMessage.Type.SPAM;
472 case BLOCK_AND_SPAM -> MessageRequestResponseMessage.Type.BLOCK_AND_SPAM;
473 };
474 }
475 }