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