1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.api
.Contact
;
4 import org
.asamk
.signal
.manager
.api
.GroupId
;
5 import org
.asamk
.signal
.manager
.api
.MessageEnvelope
.Sync
.MessageRequestResponse
;
6 import org
.asamk
.signal
.manager
.api
.TrustLevel
;
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
.RecipientAddress
;
10 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
11 import org
.asamk
.signal
.manager
.storage
.stickers
.StickerPack
;
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
.SignalServiceReceiptMessage
;
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
.DeviceContactAvatar
;
26 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
27 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
28 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
29 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
30 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
31 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.KeysMessage
;
32 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.MessageRequestResponseMessage
;
33 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ReadMessage
;
34 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
35 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
36 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.StickerPackOperationMessage
;
37 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
38 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ViewedMessage
;
39 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
40 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
41 import org
.whispersystems
.signalservice
.internal
.push
.SyncMessage
;
43 import java
.io
.FileInputStream
;
44 import java
.io
.FileOutputStream
;
45 import java
.io
.IOException
;
46 import java
.io
.InputStream
;
47 import java
.io
.OutputStream
;
48 import java
.nio
.file
.Files
;
49 import java
.util
.ArrayList
;
50 import java
.util
.List
;
51 import java
.util
.Optional
;
52 import java
.util
.stream
.Collectors
;
53 import java
.util
.stream
.Stream
;
55 public class SyncHelper
{
57 private static final Logger logger
= LoggerFactory
.getLogger(SyncHelper
.class);
59 private final Context context
;
60 private final SignalAccount account
;
62 public SyncHelper(final Context context
) {
63 this.context
= context
;
64 this.account
= context
.getAccount();
67 public void requestAllSyncData() {
68 requestSyncData(SyncMessage
.Request
.Type
.GROUPS
);
69 requestSyncData(SyncMessage
.Request
.Type
.CONTACTS
);
70 requestSyncData(SyncMessage
.Request
.Type
.BLOCKED
);
71 requestSyncData(SyncMessage
.Request
.Type
.CONFIGURATION
);
75 public void requestSyncKeys() {
76 requestSyncData(SyncMessage
.Request
.Type
.KEYS
);
79 public SendMessageResult
sendSyncFetchProfileMessage() {
80 return context
.getSendHelper()
81 .sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.LOCAL_PROFILE
));
84 public void sendSyncFetchStorageMessage() {
85 context
.getSendHelper()
86 .sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.STORAGE_MANIFEST
));
89 public void sendSyncReceiptMessage(ServiceId sender
, SignalServiceReceiptMessage receiptMessage
) {
90 if (receiptMessage
.isReadReceipt()) {
91 final var readMessages
= receiptMessage
.getTimestamps()
93 .map(t
-> new ReadMessage(sender
, t
))
95 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forRead(readMessages
));
96 } else if (receiptMessage
.isViewedReceipt()) {
97 final var viewedMessages
= receiptMessage
.getTimestamps()
99 .map(t
-> new ViewedMessage(sender
, t
))
101 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forViewed(viewedMessages
));
105 public void sendGroups() throws IOException
{
106 var groupsFile
= IOUtils
.createTempFile();
109 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
110 var out
= new DeviceGroupsOutputStream(fos
);
111 for (var record : account
.getGroupStore().getGroups()) {
112 if (record instanceof GroupInfoV1 groupInfo
) {
113 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
114 Optional
.ofNullable(groupInfo
.name
),
115 groupInfo
.getMembers()
117 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
119 context
.getGroupHelper().createGroupAvatarAttachment(groupInfo
.getGroupId()),
120 groupInfo
.isMember(account
.getSelfRecipientId()),
121 Optional
.of(groupInfo
.messageExpirationTime
),
122 Optional
.ofNullable(groupInfo
.color
),
125 groupInfo
.archived
));
130 if (groupsFile
.exists() && groupsFile
.length() > 0) {
131 try (var groupsFileStream
= new FileInputStream(groupsFile
)) {
132 final var uploadSpec
= context
.getDependencies().getMessageSender().getResumableUploadSpec();
133 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
134 .withStream(groupsFileStream
)
135 .withContentType(MimeUtils
.OCTET_STREAM
)
136 .withLength(groupsFile
.length())
137 .withResumableUploadSpec(uploadSpec
)
140 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
145 Files
.delete(groupsFile
.toPath());
146 } catch (IOException e
) {
147 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
152 public void sendContacts() throws IOException
{
153 var contactsFile
= IOUtils
.createTempFile();
156 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
157 var out
= new DeviceContactsOutputStream(fos
);
158 for (var contactPair
: account
.getContactStore().getContacts()) {
159 final var recipientId
= contactPair
.first();
160 final var contact
= contactPair
.second();
161 final var address
= account
.getRecipientAddressResolver().resolveRecipientAddress(recipientId
);
163 final var deviceContact
= getDeviceContact(address
, contact
);
164 out
.write(deviceContact
);
165 deviceContact
.getAvatar().ifPresent(a
-> {
167 a
.getInputStream().close();
168 } catch (IOException ignored
) {
173 if (account
.getProfileKey() != null) {
174 // Send our own profile key as well
175 final var address
= account
.getSelfRecipientAddress();
176 final var recipientId
= account
.getSelfRecipientId();
177 final var contact
= account
.getContactStore().getContact(recipientId
);
178 final var deviceContact
= getDeviceContact(address
, contact
);
179 out
.write(deviceContact
);
180 deviceContact
.getAvatar().ifPresent(a
-> {
182 a
.getInputStream().close();
183 } catch (IOException ignored
) {
189 if (contactsFile
.exists() && contactsFile
.length() > 0) {
190 try (var contactsFileStream
= new FileInputStream(contactsFile
)) {
191 final var uploadSpec
= context
.getDependencies().getMessageSender().getResumableUploadSpec();
192 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
193 .withStream(contactsFileStream
)
194 .withContentType(MimeUtils
.OCTET_STREAM
)
195 .withLength(contactsFile
.length())
196 .withResumableUploadSpec(uploadSpec
)
199 context
.getSendHelper()
200 .sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
,
206 Files
.delete(contactsFile
.toPath());
207 } catch (IOException e
) {
208 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
214 private DeviceContact
getDeviceContact(final RecipientAddress address
, final Contact contact
) throws IOException
{
215 return new DeviceContact(address
.aci(),
217 Optional
.ofNullable(contact
== null ?
null : contact
.getName()),
218 createContactAvatarAttachment(address
),
219 Optional
.ofNullable(contact
== null ?
null : contact
.messageExpirationTime()),
220 Optional
.ofNullable(contact
== null ?
null : contact
.messageExpirationTimeVersion()),
224 public SendMessageResult
sendBlockedList() {
225 var addresses
= new ArrayList
<BlockedListMessage
.Individual
>();
226 for (var record : account
.getContactStore().getContacts()) {
227 if (record.second().isBlocked()) {
228 final var address
= account
.getRecipientAddressResolver().resolveRecipientAddress(record.first());
229 if (address
.aci().isPresent() || address
.number().isPresent()) {
230 addresses
.add(new BlockedListMessage
.Individual(address
.aci().orElse(null),
231 address
.number().orElse(null)));
235 var groupIds
= new ArrayList
<byte[]>();
236 for (var record : account
.getGroupStore().getGroups()) {
237 if (record.isBlocked()) {
238 groupIds
.add(record.getGroupId().serialize());
241 return context
.getSendHelper()
242 .sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
245 public SendMessageResult
sendVerifiedMessage(
246 SignalServiceAddress destination
,
247 IdentityKey identityKey
,
248 TrustLevel trustLevel
250 var verifiedMessage
= new VerifiedMessage(destination
,
252 trustLevel
.toVerifiedState(),
253 System
.currentTimeMillis());
254 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
257 public SendMessageResult
sendKeysMessage() {
258 var keysMessage
= new KeysMessage(account
.getOrCreateStorageKey(),
259 account
.getOrCreatePinMasterKey(),
260 account
.getOrCreateAccountEntropyPool(),
261 account
.getOrCreateMediaRootBackupKey());
262 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forKeys(keysMessage
));
265 public SendMessageResult
sendStickerOperationsMessage(
266 List
<StickerPack
> installStickers
,
267 List
<StickerPack
> removeStickers
269 var installStickerMessages
= installStickers
.stream().map(s
-> getStickerPackOperationMessage(s
, true));
270 var removeStickerMessages
= removeStickers
.stream().map(s
-> getStickerPackOperationMessage(s
, false));
271 var stickerMessages
= Stream
.concat(installStickerMessages
, removeStickerMessages
).toList();
272 return context
.getSendHelper()
273 .sendSyncMessage(SignalServiceSyncMessage
.forStickerPackOperations(stickerMessages
));
276 private static StickerPackOperationMessage
getStickerPackOperationMessage(
278 final boolean installed
280 return new StickerPackOperationMessage(s
.packId().serialize(),
282 installed ? StickerPackOperationMessage
.Type
.INSTALL
: StickerPackOperationMessage
.Type
.REMOVE
);
285 public SendMessageResult
sendConfigurationMessage() {
286 final var config
= account
.getConfigurationStore();
287 var configurationMessage
= new ConfigurationMessage(Optional
.ofNullable(config
.getReadReceipts()),
288 Optional
.ofNullable(config
.getUnidentifiedDeliveryIndicators()),
289 Optional
.ofNullable(config
.getTypingIndicators()),
290 Optional
.ofNullable(config
.getLinkPreviews()));
291 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forConfiguration(configurationMessage
));
294 public void handleSyncDeviceGroups(final InputStream input
) {
295 final var s
= new DeviceGroupsInputStream(input
);
300 } catch (IOException e
) {
301 logger
.warn("Sync groups contained invalid group, ignoring: {}", e
.getMessage());
307 var syncGroup
= account
.getGroupStore().getOrCreateGroupV1(GroupId
.v1(g
.getId()));
308 if (syncGroup
!= null) {
309 if (g
.getName().isPresent()) {
310 syncGroup
.name
= g
.getName().get();
312 syncGroup
.addMembers(g
.getMembers()
314 .map(account
.getRecipientResolver()::resolveRecipient
)
315 .collect(Collectors
.toSet()));
317 syncGroup
.removeMember(account
.getSelfRecipientId());
319 // Add ourself to the member set as it's marked as active
320 syncGroup
.addMembers(List
.of(account
.getSelfRecipientId()));
322 syncGroup
.blocked
= g
.isBlocked();
323 if (g
.getColor().isPresent()) {
324 syncGroup
.color
= g
.getColor().get();
327 if (g
.getAvatar().isPresent()) {
328 context
.getGroupHelper().downloadGroupAvatar(syncGroup
.getGroupId(), g
.getAvatar().get());
330 syncGroup
.archived
= g
.isArchived();
331 account
.getGroupStore().updateGroup(syncGroup
);
336 public void handleSyncDeviceContacts(final InputStream input
) throws IOException
{
337 final var s
= new DeviceContactsInputStream(input
);
342 } catch (IOException e
) {
343 if (e
.getMessage() != null && e
.getMessage().contains("Missing contact address!")) {
344 logger
.debug("Sync contacts contained invalid contact, ignoring: {}", e
.getMessage());
350 if (c
== null || (c
.getAci().isEmpty() && c
.getE164().isEmpty())) {
353 final var address
= new RecipientAddress(c
.getAci(), Optional
.empty(), c
.getE164(), Optional
.empty());
354 final var recipientId
= account
.getRecipientTrustedResolver().resolveRecipientTrusted(address
);
355 var contact
= account
.getContactStore().getContact(recipientId
);
356 final var builder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
357 if (c
.getName().isPresent() && (
359 contact
.givenName() == null && contact
.familyName() == null
362 builder
.withGivenName(c
.getName().get());
363 builder
.withFamilyName(null);
365 if (c
.getExpirationTimer().isPresent()) {
366 if (c
.getExpirationTimerVersion().isPresent() && (
367 contact
== null || c
.getExpirationTimerVersion().get() > contact
.messageExpirationTimeVersion()
369 builder
.withMessageExpirationTime(c
.getExpirationTimer().get());
370 builder
.withMessageExpirationTimeVersion(c
.getExpirationTimerVersion().get());
373 "[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}",
375 c
.getExpirationTimerVersion(),
376 contact
== null ?
1 : contact
.messageExpirationTimeVersion());
379 account
.getContactStore().storeContact(recipientId
, builder
.build());
381 if (c
.getAvatar().isPresent()) {
382 storeContactAvatar(c
.getAvatar().get(), address
);
387 public SendMessageResult
sendMessageRequestResponse(final MessageRequestResponse
.Type type
, final GroupId groupId
) {
388 final var response
= MessageRequestResponseMessage
.forGroup(groupId
.serialize(), localToRemoteType(type
));
389 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forMessageRequestResponse(response
));
392 public SendMessageResult
sendMessageRequestResponse(
393 final MessageRequestResponse
.Type type
,
394 final RecipientId recipientId
396 final var address
= account
.getRecipientAddressResolver().resolveRecipientAddress(recipientId
);
397 if (address
.serviceId().isEmpty()) {
400 context
.getContactHelper()
401 .setContactProfileSharing(recipientId
,
402 type
== MessageRequestResponse
.Type
.ACCEPT
403 || type
== MessageRequestResponse
.Type
.UNBLOCK_AND_ACCEPT
);
404 final var response
= MessageRequestResponseMessage
.forIndividual(address
.serviceId().get(),
405 localToRemoteType(type
));
406 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forMessageRequestResponse(response
));
409 private SendMessageResult
requestSyncData(final SyncMessage
.Request
.Type type
) {
410 var r
= new SyncMessage
.Request
.Builder().type(type
).build();
411 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
412 return context
.getSendHelper().sendSyncMessage(message
);
415 private Optional
<DeviceContactAvatar
> createContactAvatarAttachment(RecipientAddress address
) throws IOException
{
416 final var streamDetails
= context
.getAvatarStore().retrieveContactAvatar(address
);
417 if (streamDetails
== null) {
418 return Optional
.empty();
421 return Optional
.of(new DeviceContactAvatar(streamDetails
.getStream(),
422 streamDetails
.getLength(),
423 streamDetails
.getContentType()));
426 private void storeContactAvatar(DeviceContactAvatar avatar
, RecipientAddress address
) {
428 context
.getAvatarStore()
429 .storeContactAvatar(address
,
430 outputStream
-> IOUtils
.copyStream(avatar
.getInputStream(), outputStream
));
431 } catch (IOException e
) {
432 logger
.warn("Failed to download avatar for contact {}, ignoring: {}", address
, e
.getMessage());
436 private MessageRequestResponseMessage
.Type
localToRemoteType(final MessageRequestResponse
.Type type
) {
437 return switch (type
) {
438 case UNKNOWN
-> MessageRequestResponseMessage
.Type
.UNKNOWN
;
439 case ACCEPT
-> MessageRequestResponseMessage
.Type
.ACCEPT
;
440 case DELETE
-> MessageRequestResponseMessage
.Type
.DELETE
;
441 case BLOCK
-> MessageRequestResponseMessage
.Type
.BLOCK
;
442 case BLOCK_AND_DELETE
-> MessageRequestResponseMessage
.Type
.BLOCK_AND_DELETE
;
443 case UNBLOCK_AND_ACCEPT
-> MessageRequestResponseMessage
.Type
.UNBLOCK_AND_ACCEPT
;
444 case SPAM
-> MessageRequestResponseMessage
.Type
.SPAM
;
445 case BLOCK_AND_SPAM
-> MessageRequestResponseMessage
.Type
.BLOCK_AND_SPAM
;