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