]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
Update libsignal-service-java
[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.ContactsMessage;
18 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
19 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
20 import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
21 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
22 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
23 import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
24 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
25 import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
26 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
27 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
28
29 import java.io.FileInputStream;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 import java.nio.file.Files;
35 import java.util.ArrayList;
36 import java.util.stream.Collectors;
37
38 public class SyncHelper {
39
40 private final static Logger logger = LoggerFactory.getLogger(SyncHelper.class);
41
42 private final SignalAccount account;
43 private final AttachmentHelper attachmentHelper;
44 private final SendHelper sendHelper;
45 private final GroupHelper groupHelper;
46 private final AvatarStore avatarStore;
47 private final SignalServiceAddressResolver addressResolver;
48
49 public SyncHelper(
50 final SignalAccount account,
51 final AttachmentHelper attachmentHelper,
52 final SendHelper sendHelper,
53 final GroupHelper groupHelper,
54 final AvatarStore avatarStore,
55 final SignalServiceAddressResolver addressResolver
56 ) {
57 this.account = account;
58 this.attachmentHelper = attachmentHelper;
59 this.sendHelper = sendHelper;
60 this.groupHelper = groupHelper;
61 this.avatarStore = avatarStore;
62 this.addressResolver = addressResolver;
63 }
64
65 public void requestAllSyncData() throws IOException {
66 requestSyncGroups();
67 requestSyncContacts();
68 requestSyncBlocked();
69 requestSyncConfiguration();
70 requestSyncKeys();
71 }
72
73 public void sendSyncFetchProfileMessage() throws IOException {
74 sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
75 }
76
77 public void sendGroups() throws IOException {
78 var groupsFile = IOUtils.createTempFile();
79
80 try {
81 try (OutputStream fos = new FileOutputStream(groupsFile)) {
82 var out = new DeviceGroupsOutputStream(fos);
83 for (var record : account.getGroupStore().getGroups()) {
84 if (record instanceof GroupInfoV1) {
85 var groupInfo = (GroupInfoV1) record;
86 out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
87 Optional.fromNullable(groupInfo.name),
88 groupInfo.getMembers()
89 .stream()
90 .map(addressResolver::resolveSignalServiceAddress)
91 .collect(Collectors.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 handleSyncDeviceContacts(final InputStream input) {
219 final var s = new DeviceContactsInputStream(input);
220 DeviceContact c;
221 while (true) {
222 try {
223 c = s.read();
224 } catch (IOException e) {
225 logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
226 continue;
227 }
228 if (c == null) {
229 break;
230 }
231 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
232 account.setProfileKey(c.getProfileKey().get());
233 }
234 final var recipientId = account.getRecipientStore().resolveRecipientTrusted(c.getAddress());
235 var contact = account.getContactStore().getContact(recipientId);
236 final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
237 if (c.getName().isPresent()) {
238 builder.withName(c.getName().get());
239 }
240 if (c.getColor().isPresent()) {
241 builder.withColor(c.getColor().get());
242 }
243 if (c.getProfileKey().isPresent()) {
244 account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
245 }
246 if (c.getVerified().isPresent()) {
247 final var verifiedMessage = c.getVerified().get();
248 account.getIdentityKeyStore()
249 .setIdentityTrustLevel(account.getRecipientStore()
250 .resolveRecipientTrusted(verifiedMessage.getDestination()),
251 verifiedMessage.getIdentityKey(),
252 TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
253 }
254 if (c.getExpirationTimer().isPresent()) {
255 builder.withMessageExpirationTime(c.getExpirationTimer().get());
256 }
257 builder.withBlocked(c.isBlocked());
258 builder.withArchived(c.isArchived());
259 account.getContactStore().storeContact(recipientId, builder.build());
260
261 if (c.getAvatar().isPresent()) {
262 downloadContactAvatar(c.getAvatar().get(), c.getAddress());
263 }
264 }
265 }
266
267 private void requestSyncGroups() throws IOException {
268 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
269 .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS)
270 .build();
271 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
272 sendHelper.sendSyncMessage(message);
273 }
274
275 private void requestSyncContacts() throws IOException {
276 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
277 .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS)
278 .build();
279 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
280 sendHelper.sendSyncMessage(message);
281 }
282
283 private void requestSyncBlocked() throws IOException {
284 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
285 .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED)
286 .build();
287 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
288 sendHelper.sendSyncMessage(message);
289 }
290
291 private void requestSyncConfiguration() throws IOException {
292 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
293 .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION)
294 .build();
295 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
296 sendHelper.sendSyncMessage(message);
297 }
298
299 private void requestSyncKeys() throws IOException {
300 var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
301 .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS)
302 .build();
303 var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
304 sendHelper.sendSyncMessage(message);
305 }
306
307 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
308 final var streamDetails = avatarStore.retrieveContactAvatar(address);
309 if (streamDetails == null) {
310 return Optional.absent();
311 }
312
313 return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
314 }
315
316 private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) {
317 try {
318 avatarStore.storeContactAvatar(address,
319 outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream));
320 } catch (IOException e) {
321 logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
322 }
323 }
324 }