]> 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.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 Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()),
205 Optional.empty(),
206 contact != null && contact.isArchived());
207 }
208
209 public SendMessageResult sendBlockedList() {
210 var addresses = new ArrayList<SignalServiceAddress>();
211 for (var record : account.getContactStore().getContacts()) {
212 if (record.second().isBlocked()) {
213 addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
214 }
215 }
216 var groupIds = new ArrayList<byte[]>();
217 for (var record : account.getGroupStore().getGroups()) {
218 if (record.isBlocked()) {
219 groupIds.add(record.getGroupId().serialize());
220 }
221 }
222 return context.getSendHelper()
223 .sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
224 }
225
226 public SendMessageResult sendVerifiedMessage(
227 SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
228 ) {
229 var verifiedMessage = new VerifiedMessage(destination,
230 identityKey,
231 trustLevel.toVerifiedState(),
232 System.currentTimeMillis());
233 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
234 }
235
236 public SendMessageResult sendKeysMessage() {
237 var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()),
238 Optional.ofNullable(account.getOrCreatePinMasterKey()));
239 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
240 }
241
242 public SendMessageResult sendStickerOperationsMessage(
243 List<StickerPack> installStickers, List<StickerPack> removeStickers
244 ) {
245 var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
246 var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
247 var stickerMessages = Stream.concat(installStickerMessages, removeStickerMessages).toList();
248 return context.getSendHelper()
249 .sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(stickerMessages));
250 }
251
252 private static StickerPackOperationMessage getStickerPackOperationMessage(
253 final StickerPack s, final boolean installed
254 ) {
255 return new StickerPackOperationMessage(s.packId().serialize(),
256 s.packKey(),
257 installed ? StickerPackOperationMessage.Type.INSTALL : StickerPackOperationMessage.Type.REMOVE);
258 }
259
260 public SendMessageResult sendConfigurationMessage() {
261 final var config = account.getConfigurationStore();
262 var configurationMessage = new ConfigurationMessage(Optional.ofNullable(config.getReadReceipts()),
263 Optional.ofNullable(config.getUnidentifiedDeliveryIndicators()),
264 Optional.ofNullable(config.getTypingIndicators()),
265 Optional.ofNullable(config.getLinkPreviews()));
266 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
267 }
268
269 public void handleSyncDeviceGroups(final InputStream input) {
270 final var s = new DeviceGroupsInputStream(input);
271 DeviceGroup g;
272 while (true) {
273 try {
274 g = s.read();
275 } catch (IOException e) {
276 logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
277 continue;
278 }
279 if (g == null) {
280 break;
281 }
282 var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
283 if (syncGroup != null) {
284 if (g.getName().isPresent()) {
285 syncGroup.name = g.getName().get();
286 }
287 syncGroup.addMembers(g.getMembers()
288 .stream()
289 .map(account.getRecipientResolver()::resolveRecipient)
290 .collect(Collectors.toSet()));
291 if (!g.isActive()) {
292 syncGroup.removeMember(account.getSelfRecipientId());
293 } else {
294 // Add ourself to the member set as it's marked as active
295 syncGroup.addMembers(List.of(account.getSelfRecipientId()));
296 }
297 syncGroup.blocked = g.isBlocked();
298 if (g.getColor().isPresent()) {
299 syncGroup.color = g.getColor().get();
300 }
301
302 if (g.getAvatar().isPresent()) {
303 context.getGroupHelper().downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
304 }
305 syncGroup.archived = g.isArchived();
306 account.getGroupStore().updateGroup(syncGroup);
307 }
308 }
309 }
310
311 public void handleSyncDeviceContacts(final InputStream input) throws IOException {
312 final var s = new DeviceContactsInputStream(input);
313 DeviceContact c;
314 while (true) {
315 try {
316 c = s.read();
317 } catch (IOException e) {
318 if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) {
319 logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
320 continue;
321 } else {
322 throw e;
323 }
324 }
325 if (c == null || (c.getAci().isEmpty() && c.getE164().isEmpty())) {
326 break;
327 }
328 final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty());
329 if (address.matches(account.getSelfRecipientAddress()) && c.getProfileKey().isPresent()) {
330 account.setProfileKey(c.getProfileKey().get());
331 }
332 final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address);
333 var contact = account.getContactStore().getContact(recipientId);
334 final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
335 if (c.getName().isPresent() && (
336 contact == null || (
337 contact.givenName() == null && contact.familyName() == null
338 )
339 )) {
340 builder.withGivenName(c.getName().get());
341 builder.withFamilyName(null);
342 }
343 if (c.getColor().isPresent()) {
344 builder.withColor(c.getColor().get());
345 }
346 if (c.getProfileKey().isPresent()) {
347 account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
348 }
349 if (c.getVerified().isPresent()) {
350 final var verifiedMessage = c.getVerified().get();
351 account.getIdentityKeyStore()
352 .setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
353 verifiedMessage.getIdentityKey(),
354 TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
355 }
356 if (c.getExpirationTimer().isPresent()) {
357 builder.withMessageExpirationTime(c.getExpirationTimer().get());
358 }
359 builder.withIsArchived(c.isArchived());
360 account.getContactStore().storeContact(recipientId, builder.build());
361
362 if (c.getAvatar().isPresent()) {
363 downloadContactAvatar(c.getAvatar().get(), address);
364 }
365 }
366 }
367
368 public SendMessageResult sendMessageRequestResponse(
369 final MessageRequestResponse.Type type, final GroupId groupId
370 ) {
371 final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type));
372 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
373 }
374
375 public SendMessageResult sendMessageRequestResponse(
376 final MessageRequestResponse.Type type, final RecipientId recipientId
377 ) {
378 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
379 if (address.serviceId().isEmpty()) {
380 return null;
381 }
382 context.getContactHelper()
383 .setContactProfileSharing(recipientId,
384 type == MessageRequestResponse.Type.ACCEPT
385 || type == MessageRequestResponse.Type.UNBLOCK_AND_ACCEPT);
386 final var response = MessageRequestResponseMessage.forIndividual(address.serviceId().get(),
387 localToRemoteType(type));
388 return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
389 }
390
391 private SendMessageResult requestSyncData(final SyncMessage.Request.Type type) {
392 var r = new SyncMessage.Request.Builder().type(type).build();
393 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
394 return context.getSendHelper().sendSyncMessage(message);
395 }
396
397 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(RecipientAddress address) throws IOException {
398 final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address);
399 if (streamDetails == null) {
400 return Optional.empty();
401 }
402
403 return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty()));
404 }
405
406 private void downloadContactAvatar(SignalServiceAttachment avatar, RecipientAddress address) {
407 try {
408 context.getAvatarStore()
409 .storeContactAvatar(address,
410 outputStream -> context.getAttachmentHelper().retrieveAttachment(avatar, outputStream));
411 } catch (IOException e) {
412 logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
413 }
414 }
415
416 private MessageRequestResponseMessage.Type localToRemoteType(final MessageRequestResponse.Type type) {
417 return switch (type) {
418 case UNKNOWN -> MessageRequestResponseMessage.Type.UNKNOWN;
419 case ACCEPT -> MessageRequestResponseMessage.Type.ACCEPT;
420 case DELETE -> MessageRequestResponseMessage.Type.DELETE;
421 case BLOCK -> MessageRequestResponseMessage.Type.BLOCK;
422 case BLOCK_AND_DELETE -> MessageRequestResponseMessage.Type.BLOCK_AND_DELETE;
423 case UNBLOCK_AND_ACCEPT -> MessageRequestResponseMessage.Type.UNBLOCK_AND_ACCEPT;
424 case SPAM -> MessageRequestResponseMessage.Type.SPAM;
425 case BLOCK_AND_SPAM -> MessageRequestResponseMessage.Type.BLOCK_AND_SPAM;
426 };
427 }
428 }