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