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