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