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