]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
Extract AttachmentHelper and SyncHelper
[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.AvatarStore;
4 import org.asamk.signal.manager.TrustLevel;
5 import org.asamk.signal.manager.groups.GroupId;
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.Contact;
9 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
10 import org.asamk.signal.manager.util.AttachmentUtils;
11 import org.asamk.signal.manager.util.IOUtils;
12 import org.slf4j.Logger;
13 import org.slf4j.LoggerFactory;
14 import org.whispersystems.libsignal.IdentityKey;
15 import org.whispersystems.libsignal.util.guava.Optional;
16 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
17 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
18 import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
19 import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
20 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
21 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
22 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
23 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
24 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
25 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
26 import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
27 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
28 import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
29 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
30 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
31
32 import java.io.FileInputStream;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.nio.file.Files;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.stream.Collectors;
41
42 public class SyncHelper {
43
44 private final static Logger logger = LoggerFactory.getLogger(SyncHelper.class);
45
46 private final SignalAccount account;
47 private final AttachmentHelper attachmentHelper;
48 private final SendHelper sendHelper;
49 private final GroupHelper groupHelper;
50 private final AvatarStore avatarStore;
51 private final SignalServiceAddressResolver addressResolver;
52 private final RecipientResolver recipientResolver;
53
54 public SyncHelper(
55 final SignalAccount account,
56 final AttachmentHelper attachmentHelper,
57 final SendHelper sendHelper,
58 final GroupHelper groupHelper,
59 final AvatarStore avatarStore,
60 final SignalServiceAddressResolver addressResolver,
61 final RecipientResolver recipientResolver
62 ) {
63 this.account = account;
64 this.attachmentHelper = attachmentHelper;
65 this.sendHelper = sendHelper;
66 this.groupHelper = groupHelper;
67 this.avatarStore = avatarStore;
68 this.addressResolver = addressResolver;
69 this.recipientResolver = recipientResolver;
70 }
71
72 public void requestAllSyncData() throws IOException {
73 requestSyncGroups();
74 requestSyncContacts();
75 requestSyncBlocked();
76 requestSyncConfiguration();
77 requestSyncKeys();
78 }
79
80 public void sendSyncFetchProfileMessage() throws IOException {
81 sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
82 }
83
84 public void sendGroups() throws IOException {
85 var groupsFile = IOUtils.createTempFile();
86
87 try {
88 try (OutputStream fos = new FileOutputStream(groupsFile)) {
89 var out = new DeviceGroupsOutputStream(fos);
90 for (var record : account.getGroupStore().getGroups()) {
91 if (record instanceof GroupInfoV1) {
92 var groupInfo = (GroupInfoV1) record;
93 out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
94 Optional.fromNullable(groupInfo.name),
95 groupInfo.getMembers()
96 .stream()
97 .map(addressResolver::resolveSignalServiceAddress)
98 .collect(Collectors.toList()),
99 groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()),
100 groupInfo.isMember(account.getSelfRecipientId()),
101 Optional.of(groupInfo.messageExpirationTime),
102 Optional.fromNullable(groupInfo.color),
103 groupInfo.blocked,
104 Optional.absent(),
105 groupInfo.archived));
106 }
107 }
108 }
109
110 if (groupsFile.exists() && groupsFile.length() > 0) {
111 try (var groupsFileStream = new FileInputStream(groupsFile)) {
112 var attachmentStream = SignalServiceAttachment.newStreamBuilder()
113 .withStream(groupsFileStream)
114 .withContentType("application/octet-stream")
115 .withLength(groupsFile.length())
116 .build();
117
118 sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
119 }
120 }
121 } finally {
122 try {
123 Files.delete(groupsFile.toPath());
124 } catch (IOException e) {
125 logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage());
126 }
127 }
128 }
129
130 public void sendContacts() throws IOException {
131 var contactsFile = IOUtils.createTempFile();
132
133 try {
134 try (OutputStream fos = new FileOutputStream(contactsFile)) {
135 var out = new DeviceContactsOutputStream(fos);
136 for (var contactPair : account.getContactStore().getContacts()) {
137 final var recipientId = contactPair.first();
138 final var contact = contactPair.second();
139 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
140
141 var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId);
142 VerifiedMessage verifiedMessage = null;
143 if (currentIdentity != null) {
144 verifiedMessage = new VerifiedMessage(address,
145 currentIdentity.getIdentityKey(),
146 currentIdentity.getTrustLevel().toVerifiedState(),
147 currentIdentity.getDateAdded().getTime());
148 }
149
150 var profileKey = account.getProfileStore().getProfileKey(recipientId);
151 out.write(new DeviceContact(address,
152 Optional.fromNullable(contact.getName()),
153 createContactAvatarAttachment(address),
154 Optional.fromNullable(contact.getColor()),
155 Optional.fromNullable(verifiedMessage),
156 Optional.fromNullable(profileKey),
157 contact.isBlocked(),
158 Optional.of(contact.getMessageExpirationTime()),
159 Optional.absent(),
160 contact.isArchived()));
161 }
162
163 if (account.getProfileKey() != null) {
164 // Send our own profile key as well
165 out.write(new DeviceContact(account.getSelfAddress(),
166 Optional.absent(),
167 Optional.absent(),
168 Optional.absent(),
169 Optional.absent(),
170 Optional.of(account.getProfileKey()),
171 false,
172 Optional.absent(),
173 Optional.absent(),
174 false));
175 }
176 }
177
178 if (contactsFile.exists() && contactsFile.length() > 0) {
179 try (var contactsFileStream = new FileInputStream(contactsFile)) {
180 var attachmentStream = SignalServiceAttachment.newStreamBuilder()
181 .withStream(contactsFileStream)
182 .withContentType("application/octet-stream")
183 .withLength(contactsFile.length())
184 .build();
185
186 sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream,
187 true)));
188 }
189 }
190 } finally {
191 try {
192 Files.delete(contactsFile.toPath());
193 } catch (IOException e) {
194 logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage());
195 }
196 }
197 }
198
199 public void sendBlockedList() throws IOException {
200 var addresses = new ArrayList<SignalServiceAddress>();
201 for (var record : account.getContactStore().getContacts()) {
202 if (record.second().isBlocked()) {
203 addresses.add(addressResolver.resolveSignalServiceAddress(record.first()));
204 }
205 }
206 var groupIds = new ArrayList<byte[]>();
207 for (var record : account.getGroupStore().getGroups()) {
208 if (record.isBlocked()) {
209 groupIds.add(record.getGroupId().serialize());
210 }
211 }
212 sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
213 }
214
215 public void sendVerifiedMessage(
216 SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
217 ) throws IOException {
218 var verifiedMessage = new VerifiedMessage(destination,
219 identityKey,
220 trustLevel.toVerifiedState(),
221 System.currentTimeMillis());
222 sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
223 }
224
225 public void handleSyncDeviceGroups(final InputStream input) {
226 final var s = new DeviceGroupsInputStream(input);
227 DeviceGroup g;
228 while (true) {
229 try {
230 g = s.read();
231 } catch (IOException e) {
232 logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
233 continue;
234 }
235 if (g == null) {
236 break;
237 }
238 var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
239 if (syncGroup != null) {
240 if (g.getName().isPresent()) {
241 syncGroup.name = g.getName().get();
242 }
243 syncGroup.addMembers(g.getMembers()
244 .stream()
245 .map(recipientResolver::resolveRecipient)
246 .collect(Collectors.toSet()));
247 if (!g.isActive()) {
248 syncGroup.removeMember(account.getSelfRecipientId());
249 } else {
250 // Add ourself to the member set as it's marked as active
251 syncGroup.addMembers(List.of(account.getSelfRecipientId()));
252 }
253 syncGroup.blocked = g.isBlocked();
254 if (g.getColor().isPresent()) {
255 syncGroup.color = g.getColor().get();
256 }
257
258 if (g.getAvatar().isPresent()) {
259 groupHelper.downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
260 }
261 syncGroup.archived = g.isArchived();
262 account.getGroupStore().updateGroup(syncGroup);
263 }
264 }
265 }
266
267 public void handleSyncDeviceContacts(final InputStream input) {
268 final var s = new DeviceContactsInputStream(input);
269 DeviceContact c;
270 while (true) {
271 try {
272 c = s.read();
273 } catch (IOException e) {
274 logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
275 continue;
276 }
277 if (c == null) {
278 break;
279 }
280 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
281 account.setProfileKey(c.getProfileKey().get());
282 }
283 final var recipientId = account.getRecipientStore().resolveRecipientTrusted(c.getAddress());
284 var contact = account.getContactStore().getContact(recipientId);
285 final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
286 if (c.getName().isPresent()) {
287 builder.withName(c.getName().get());
288 }
289 if (c.getColor().isPresent()) {
290 builder.withColor(c.getColor().get());
291 }
292 if (c.getProfileKey().isPresent()) {
293 account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
294 }
295 if (c.getVerified().isPresent()) {
296 final var verifiedMessage = c.getVerified().get();
297 account.getIdentityKeyStore()
298 .setIdentityTrustLevel(account.getRecipientStore()
299 .resolveRecipientTrusted(verifiedMessage.getDestination()),
300 verifiedMessage.getIdentityKey(),
301 TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
302 }
303 if (c.getExpirationTimer().isPresent()) {
304 builder.withMessageExpirationTime(c.getExpirationTimer().get());
305 }
306 builder.withBlocked(c.isBlocked());
307 builder.withArchived(c.isArchived());
308 account.getContactStore().storeContact(recipientId, builder.build());
309
310 if (c.getAvatar().isPresent()) {
311 downloadContactAvatar(c.getAvatar().get(), c.getAddress());
312 }
313 }
314 }
315
316 private void requestSyncGroups() throws IOException {
317 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
318 .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS)
319 .build();
320 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
321 sendHelper.sendSyncMessage(message);
322 }
323
324 private void requestSyncContacts() throws IOException {
325 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
326 .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS)
327 .build();
328 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
329 sendHelper.sendSyncMessage(message);
330 }
331
332 private void requestSyncBlocked() throws IOException {
333 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
334 .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED)
335 .build();
336 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
337 sendHelper.sendSyncMessage(message);
338 }
339
340 private void requestSyncConfiguration() throws IOException {
341 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
342 .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION)
343 .build();
344 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
345 sendHelper.sendSyncMessage(message);
346 }
347
348 private void requestSyncKeys() throws IOException {
349 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
350 .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS)
351 .build();
352 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
353 sendHelper.sendSyncMessage(message);
354 }
355
356 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
357 final var streamDetails = avatarStore.retrieveContactAvatar(address);
358 if (streamDetails == null) {
359 return Optional.absent();
360 }
361
362 return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
363 }
364
365 private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) {
366 try {
367 avatarStore.storeContactAvatar(address,
368 outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream));
369 } catch (IOException e) {
370 logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
371 }
372 }
373 }