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
.AttachmentUtils
;
13 import org
.asamk
.signal
.manager
.util
.IOUtils
;
14 import org
.asamk
.signal
.manager
.util
.MimeUtils
;
15 import org
.jetbrains
.annotations
.NotNull
;
16 import org
.signal
.libsignal
.protocol
.IdentityKey
;
17 import org
.slf4j
.Logger
;
18 import org
.slf4j
.LoggerFactory
;
19 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
20 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
21 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
22 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
23 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
24 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ConfigurationMessage
;
25 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
26 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
27 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
28 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
29 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
30 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
31 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
32 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.KeysMessage
;
33 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.MessageRequestResponseMessage
;
34 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ReadMessage
;
35 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
36 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
37 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.StickerPackOperationMessage
;
38 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
39 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ViewedMessage
;
40 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
41 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
42 import org
.whispersystems
.signalservice
.internal
.push
.SyncMessage
;
43 import org
.whispersystems
.signalservice
.internal
.push
.http
.ResumableUploadSpec
;
45 import java
.io
.FileInputStream
;
46 import java
.io
.FileOutputStream
;
47 import java
.io
.IOException
;
48 import java
.io
.InputStream
;
49 import java
.io
.OutputStream
;
50 import java
.nio
.file
.Files
;
51 import java
.util
.ArrayList
;
52 import java
.util
.List
;
53 import java
.util
.Optional
;
54 import java
.util
.stream
.Collectors
;
55 import java
.util
.stream
.Stream
;
57 public class SyncHelper
{
59 private static final Logger logger
= LoggerFactory
.getLogger(SyncHelper
.class);
61 private final Context context
;
62 private final SignalAccount account
;
64 public SyncHelper(final Context context
) {
65 this.context
= context
;
66 this.account
= context
.getAccount();
69 public void requestAllSyncData() {
70 requestSyncData(SyncMessage
.Request
.Type
.GROUPS
);
71 requestSyncData(SyncMessage
.Request
.Type
.CONTACTS
);
72 requestSyncData(SyncMessage
.Request
.Type
.BLOCKED
);
73 requestSyncData(SyncMessage
.Request
.Type
.CONFIGURATION
);
75 requestSyncPniIdentity();
78 public void requestSyncKeys() {
79 requestSyncData(SyncMessage
.Request
.Type
.KEYS
);
82 public void requestSyncPniIdentity() {
83 requestSyncData(SyncMessage
.Request
.Type
.PNI_IDENTITY
);
86 public SendMessageResult
sendSyncFetchProfileMessage() {
87 return context
.getSendHelper()
88 .sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.LOCAL_PROFILE
));
91 public void sendSyncFetchStorageMessage() {
92 context
.getSendHelper()
93 .sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.STORAGE_MANIFEST
));
96 public void sendSyncReceiptMessage(ServiceId sender
, SignalServiceReceiptMessage receiptMessage
) {
97 if (receiptMessage
.isReadReceipt()) {
98 final var readMessages
= receiptMessage
.getTimestamps()
100 .map(t
-> new ReadMessage(sender
, t
))
102 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forRead(readMessages
));
103 } else if (receiptMessage
.isViewedReceipt()) {
104 final var viewedMessages
= receiptMessage
.getTimestamps()
106 .map(t
-> new ViewedMessage(sender
, t
))
108 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forViewed(viewedMessages
));
112 public void sendGroups() throws IOException
{
113 var groupsFile
= IOUtils
.createTempFile();
116 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
117 var out
= new DeviceGroupsOutputStream(fos
);
118 for (var record : account
.getGroupStore().getGroups()) {
119 if (record instanceof GroupInfoV1 groupInfo
) {
120 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
121 Optional
.ofNullable(groupInfo
.name
),
122 groupInfo
.getMembers()
124 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
126 context
.getGroupHelper().createGroupAvatarAttachment(groupInfo
.getGroupId()),
127 groupInfo
.isMember(account
.getSelfRecipientId()),
128 Optional
.of(groupInfo
.messageExpirationTime
),
129 Optional
.ofNullable(groupInfo
.color
),
132 groupInfo
.archived
));
137 if (groupsFile
.exists() && groupsFile
.length() > 0) {
138 try (var groupsFileStream
= new FileInputStream(groupsFile
)) {
139 final var uploadSpec
= context
.getDependencies()
141 .getResumableUploadSpec()
143 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
144 .withStream(groupsFileStream
)
145 .withContentType(MimeUtils
.OCTET_STREAM
)
146 .withLength(groupsFile
.length())
147 .withResumableUploadSpec(ResumableUploadSpec
.from(uploadSpec
))
150 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
155 Files
.delete(groupsFile
.toPath());
156 } catch (IOException e
) {
157 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
162 public void sendContacts() throws IOException
{
163 var contactsFile
= IOUtils
.createTempFile();
166 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
167 var out
= new DeviceContactsOutputStream(fos
);
168 for (var contactPair
: account
.getContactStore().getContacts()) {
169 final var recipientId
= contactPair
.first();
170 final var contact
= contactPair
.second();
171 final var address
= account
.getRecipientAddressResolver().resolveRecipientAddress(recipientId
);
173 out
.write(getDeviceContact(address
, recipientId
, contact
));
176 if (account
.getProfileKey() != null) {
177 // Send our own profile key as well
178 final var address
= account
.getSelfRecipientAddress();
179 final var recipientId
= account
.getSelfRecipientId();
180 final var contact
= account
.getContactStore().getContact(recipientId
);
181 out
.write(getDeviceContact(address
, recipientId
, contact
));
185 if (contactsFile
.exists() && contactsFile
.length() > 0) {
186 try (var contactsFileStream
= new FileInputStream(contactsFile
)) {
187 final var uploadSpec
= context
.getDependencies()
189 .getResumableUploadSpec()
191 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
192 .withStream(contactsFileStream
)
193 .withContentType(MimeUtils
.OCTET_STREAM
)
194 .withLength(contactsFile
.length())
195 .withResumableUploadSpec(ResumableUploadSpec
.from(uploadSpec
))
198 context
.getSendHelper()
199 .sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
,
205 Files
.delete(contactsFile
.toPath());
206 } catch (IOException e
) {
207 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
213 private DeviceContact
getDeviceContact(
214 final RecipientAddress address
, final RecipientId recipientId
, final Contact contact
215 ) throws IOException
{
216 var currentIdentity
= address
.serviceId().isEmpty()
218 : account
.getIdentityKeyStore().getIdentityInfo(address
.serviceId().get());
219 VerifiedMessage verifiedMessage
= null;
220 if (currentIdentity
!= null) {
221 verifiedMessage
= new VerifiedMessage(address
.toSignalServiceAddress(),
222 currentIdentity
.getIdentityKey(),
223 currentIdentity
.getTrustLevel().toVerifiedState(),
224 currentIdentity
.getDateAddedTimestamp());
227 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
228 return new DeviceContact(address
.aci(),
230 Optional
.ofNullable(contact
== null ?
null : contact
.getName()),
231 createContactAvatarAttachment(address
),
232 Optional
.ofNullable(contact
== null ?
null : contact
.color()),
233 Optional
.ofNullable(verifiedMessage
),
234 Optional
.ofNullable(profileKey
),
235 Optional
.ofNullable(contact
== null ?
null : contact
.messageExpirationTime()),
237 contact
!= null && contact
.isArchived());
240 public SendMessageResult
sendBlockedList() {
241 var addresses
= new ArrayList
<SignalServiceAddress
>();
242 for (var record : account
.getContactStore().getContacts()) {
243 if (record.second().isBlocked()) {
244 addresses
.add(context
.getRecipientHelper().resolveSignalServiceAddress(record.first()));
247 var groupIds
= new ArrayList
<byte[]>();
248 for (var record : account
.getGroupStore().getGroups()) {
249 if (record.isBlocked()) {
250 groupIds
.add(record.getGroupId().serialize());
253 return context
.getSendHelper()
254 .sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
257 public SendMessageResult
sendVerifiedMessage(
258 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
260 var verifiedMessage
= new VerifiedMessage(destination
,
262 trustLevel
.toVerifiedState(),
263 System
.currentTimeMillis());
264 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
267 public SendMessageResult
sendKeysMessage() {
268 var keysMessage
= new KeysMessage(Optional
.ofNullable(account
.getOrCreateStorageKey()),
269 Optional
.ofNullable(account
.getOrCreatePinMasterKey()));
270 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forKeys(keysMessage
));
273 public SendMessageResult
sendStickerOperationsMessage(
274 List
<StickerPack
> installStickers
, List
<StickerPack
> removeStickers
276 var installStickerMessages
= installStickers
.stream().map(s
-> getStickerPackOperationMessage(s
, true));
277 var removeStickerMessages
= removeStickers
.stream().map(s
-> getStickerPackOperationMessage(s
, false));
278 var stickerMessages
= Stream
.concat(installStickerMessages
, removeStickerMessages
).toList();
279 return context
.getSendHelper()
280 .sendSyncMessage(SignalServiceSyncMessage
.forStickerPackOperations(stickerMessages
));
283 private static StickerPackOperationMessage
getStickerPackOperationMessage(
284 final StickerPack s
, final boolean installed
286 return new StickerPackOperationMessage(s
.packId().serialize(),
288 installed ? StickerPackOperationMessage
.Type
.INSTALL
: StickerPackOperationMessage
.Type
.REMOVE
);
291 public SendMessageResult
sendConfigurationMessage() {
292 final var config
= account
.getConfigurationStore();
293 var configurationMessage
= new ConfigurationMessage(Optional
.ofNullable(config
.getReadReceipts()),
294 Optional
.ofNullable(config
.getUnidentifiedDeliveryIndicators()),
295 Optional
.ofNullable(config
.getTypingIndicators()),
296 Optional
.ofNullable(config
.getLinkPreviews()));
297 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forConfiguration(configurationMessage
));
300 public void handleSyncDeviceGroups(final InputStream input
) {
301 final var s
= new DeviceGroupsInputStream(input
);
306 } catch (IOException e
) {
307 logger
.warn("Sync groups contained invalid group, ignoring: {}", e
.getMessage());
313 var syncGroup
= account
.getGroupStore().getOrCreateGroupV1(GroupId
.v1(g
.getId()));
314 if (syncGroup
!= null) {
315 if (g
.getName().isPresent()) {
316 syncGroup
.name
= g
.getName().get();
318 syncGroup
.addMembers(g
.getMembers()
320 .map(account
.getRecipientResolver()::resolveRecipient
)
321 .collect(Collectors
.toSet()));
323 syncGroup
.removeMember(account
.getSelfRecipientId());
325 // Add ourself to the member set as it's marked as active
326 syncGroup
.addMembers(List
.of(account
.getSelfRecipientId()));
328 syncGroup
.blocked
= g
.isBlocked();
329 if (g
.getColor().isPresent()) {
330 syncGroup
.color
= g
.getColor().get();
333 if (g
.getAvatar().isPresent()) {
334 context
.getGroupHelper().downloadGroupAvatar(syncGroup
.getGroupId(), g
.getAvatar().get());
336 syncGroup
.archived
= g
.isArchived();
337 account
.getGroupStore().updateGroup(syncGroup
);
342 public void handleSyncDeviceContacts(final InputStream input
) throws IOException
{
343 final var s
= new DeviceContactsInputStream(input
);
348 } catch (IOException e
) {
349 if (e
.getMessage() != null && e
.getMessage().contains("Missing contact address!")) {
350 logger
.warn("Sync contacts contained invalid contact, ignoring: {}", e
.getMessage());
356 if (c
== null || (c
.getAci().isEmpty() && c
.getE164().isEmpty())) {
359 final var address
= new RecipientAddress(c
.getAci(), Optional
.empty(), c
.getE164(), Optional
.empty());
360 if (address
.matches(account
.getSelfRecipientAddress()) && c
.getProfileKey().isPresent()) {
361 account
.setProfileKey(c
.getProfileKey().get());
363 final var recipientId
= account
.getRecipientTrustedResolver().resolveRecipientTrusted(address
);
364 var contact
= account
.getContactStore().getContact(recipientId
);
365 final var builder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
366 if (c
.getName().isPresent() && (
368 contact
.givenName() == null && contact
.familyName() == null
371 builder
.withGivenName(c
.getName().get());
372 builder
.withFamilyName(null);
374 if (c
.getColor().isPresent()) {
375 builder
.withColor(c
.getColor().get());
377 if (c
.getProfileKey().isPresent()) {
378 account
.getProfileStore().storeProfileKey(recipientId
, c
.getProfileKey().get());
380 if (c
.getVerified().isPresent()) {
381 final var verifiedMessage
= c
.getVerified().get();
382 account
.getIdentityKeyStore()
383 .setIdentityTrustLevel(verifiedMessage
.getDestination().getServiceId(),
384 verifiedMessage
.getIdentityKey(),
385 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
387 if (c
.getExpirationTimer().isPresent()) {
388 builder
.withMessageExpirationTime(c
.getExpirationTimer().get());
390 builder
.withIsArchived(c
.isArchived());
391 account
.getContactStore().storeContact(recipientId
, builder
.build());
393 if (c
.getAvatar().isPresent()) {
394 downloadContactAvatar(c
.getAvatar().get(), address
);
399 public SendMessageResult
sendMessageRequestResponse(
400 final MessageRequestResponse
.Type type
, final GroupId groupId
402 final var response
= MessageRequestResponseMessage
.forGroup(groupId
.serialize(), localToRemoteType(type
));
403 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forMessageRequestResponse(response
));
406 public SendMessageResult
sendMessageRequestResponse(
407 final MessageRequestResponse
.Type type
, final RecipientId recipientId
409 final var address
= account
.getRecipientAddressResolver().resolveRecipientAddress(recipientId
);
410 if (address
.serviceId().isEmpty()) {
413 context
.getContactHelper()
414 .setContactProfileSharing(recipientId
,
415 type
== MessageRequestResponse
.Type
.ACCEPT
416 || type
== MessageRequestResponse
.Type
.UNBLOCK_AND_ACCEPT
);
417 final var response
= MessageRequestResponseMessage
.forIndividual(address
.serviceId().get(),
418 localToRemoteType(type
));
419 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forMessageRequestResponse(response
));
422 private SendMessageResult
requestSyncData(final SyncMessage
.Request
.Type type
) {
423 var r
= new SyncMessage
.Request
.Builder().type(type
).build();
424 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
425 return context
.getSendHelper().sendSyncMessage(message
);
428 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(RecipientAddress address
) throws IOException
{
429 final var streamDetails
= context
.getAvatarStore().retrieveContactAvatar(address
);
430 if (streamDetails
== null) {
431 return Optional
.empty();
434 final var uploadSpec
= context
.getDependencies().getMessageSender().getResumableUploadSpec().toProto();
435 return Optional
.of(AttachmentUtils
.createAttachmentStream(streamDetails
, Optional
.empty(), uploadSpec
));
438 private void downloadContactAvatar(SignalServiceAttachment avatar
, RecipientAddress address
) {
440 context
.getAvatarStore()
441 .storeContactAvatar(address
,
442 outputStream
-> context
.getAttachmentHelper().retrieveAttachment(avatar
, outputStream
));
443 } catch (IOException e
) {
444 logger
.warn("Failed to download avatar for contact {}, ignoring: {}", address
, e
.getMessage());
448 private MessageRequestResponseMessage
.Type
localToRemoteType(final MessageRequestResponse
.Type type
) {
449 return switch (type
) {
450 case UNKNOWN
-> MessageRequestResponseMessage
.Type
.UNKNOWN
;
451 case ACCEPT
-> MessageRequestResponseMessage
.Type
.ACCEPT
;
452 case DELETE
-> MessageRequestResponseMessage
.Type
.DELETE
;
453 case BLOCK
-> MessageRequestResponseMessage
.Type
.BLOCK
;
454 case BLOCK_AND_DELETE
-> MessageRequestResponseMessage
.Type
.BLOCK_AND_DELETE
;
455 case UNBLOCK_AND_ACCEPT
-> MessageRequestResponseMessage
.Type
.UNBLOCK_AND_ACCEPT
;
456 case SPAM
-> MessageRequestResponseMessage
.Type
.SPAM
;
457 case BLOCK_AND_SPAM
-> MessageRequestResponseMessage
.Type
.BLOCK_AND_SPAM
;