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