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
.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
.SendMessageResult
;
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
.StickerPackOperationMessage
;
32 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
33 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
34 import org
.whispersystems
.signalservice
.internal
.push
.SyncMessage
;
36 import java
.io
.FileInputStream
;
37 import java
.io
.FileOutputStream
;
38 import java
.io
.IOException
;
39 import java
.io
.InputStream
;
40 import java
.io
.OutputStream
;
41 import java
.nio
.file
.Files
;
42 import java
.util
.ArrayList
;
43 import java
.util
.List
;
44 import java
.util
.Optional
;
45 import java
.util
.stream
.Collectors
;
46 import java
.util
.stream
.Stream
;
48 public class SyncHelper
{
50 private final static Logger logger
= LoggerFactory
.getLogger(SyncHelper
.class);
52 private final Context context
;
53 private final SignalAccount account
;
55 public SyncHelper(final Context context
) {
56 this.context
= context
;
57 this.account
= context
.getAccount();
60 public void requestAllSyncData() {
61 requestSyncData(SyncMessage
.Request
.Type
.GROUPS
);
62 requestSyncData(SyncMessage
.Request
.Type
.CONTACTS
);
63 requestSyncData(SyncMessage
.Request
.Type
.BLOCKED
);
64 requestSyncData(SyncMessage
.Request
.Type
.CONFIGURATION
);
66 requestSyncPniIdentity();
69 public void requestSyncKeys() {
70 requestSyncData(SyncMessage
.Request
.Type
.KEYS
);
73 public void requestSyncPniIdentity() {
74 requestSyncData(SyncMessage
.Request
.Type
.PNI_IDENTITY
);
77 public SendMessageResult
sendSyncFetchProfileMessage() {
78 return context
.getSendHelper()
79 .sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.LOCAL_PROFILE
));
82 public void sendGroups() throws IOException
{
83 var groupsFile
= IOUtils
.createTempFile();
86 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
87 var out
= new DeviceGroupsOutputStream(fos
);
88 for (var record : account
.getGroupStore().getGroups()) {
89 if (record instanceof GroupInfoV1 groupInfo
) {
90 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
91 Optional
.ofNullable(groupInfo
.name
),
92 groupInfo
.getMembers()
94 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
96 context
.getGroupHelper().createGroupAvatarAttachment(groupInfo
.getGroupId()),
97 groupInfo
.isMember(account
.getSelfRecipientId()),
98 Optional
.of(groupInfo
.messageExpirationTime
),
99 Optional
.ofNullable(groupInfo
.color
),
102 groupInfo
.archived
));
107 if (groupsFile
.exists() && groupsFile
.length() > 0) {
108 try (var groupsFileStream
= new FileInputStream(groupsFile
)) {
109 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
110 .withStream(groupsFileStream
)
111 .withContentType(MimeUtils
.OCTET_STREAM
)
112 .withLength(groupsFile
.length())
115 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
120 Files
.delete(groupsFile
.toPath());
121 } catch (IOException e
) {
122 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
127 public void sendContacts() throws IOException
{
128 var contactsFile
= IOUtils
.createTempFile();
131 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
132 var out
= new DeviceContactsOutputStream(fos
);
133 for (var contactPair
: account
.getContactStore().getContacts()) {
134 final var recipientId
= contactPair
.first();
135 final var contact
= contactPair
.second();
136 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
138 var currentIdentity
= account
.getIdentityKeyStore().getIdentityInfo(address
.getServiceId());
139 VerifiedMessage verifiedMessage
= null;
140 if (currentIdentity
!= null) {
141 verifiedMessage
= new VerifiedMessage(address
,
142 currentIdentity
.getIdentityKey(),
143 currentIdentity
.getTrustLevel().toVerifiedState(),
144 currentIdentity
.getDateAddedTimestamp());
147 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
148 out
.write(new DeviceContact(address
,
149 Optional
.ofNullable(contact
.getName()),
150 createContactAvatarAttachment(new RecipientAddress(address
)),
151 Optional
.ofNullable(contact
.getColor()),
152 Optional
.ofNullable(verifiedMessage
),
153 Optional
.ofNullable(profileKey
),
155 Optional
.of(contact
.getMessageExpirationTime()),
157 contact
.isArchived()));
160 if (account
.getProfileKey() != null) {
161 // Send our own profile key as well
162 out
.write(new DeviceContact(account
.getSelfAddress(),
167 Optional
.of(account
.getProfileKey()),
175 if (contactsFile
.exists() && contactsFile
.length() > 0) {
176 try (var contactsFileStream
= new FileInputStream(contactsFile
)) {
177 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
178 .withStream(contactsFileStream
)
179 .withContentType(MimeUtils
.OCTET_STREAM
)
180 .withLength(contactsFile
.length())
183 context
.getSendHelper()
184 .sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
,
190 Files
.delete(contactsFile
.toPath());
191 } catch (IOException e
) {
192 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
197 public SendMessageResult
sendBlockedList() {
198 var addresses
= new ArrayList
<SignalServiceAddress
>();
199 for (var record : account
.getContactStore().getContacts()) {
200 if (record.second().isBlocked()) {
201 addresses
.add(context
.getRecipientHelper().resolveSignalServiceAddress(record.first()));
204 var groupIds
= new ArrayList
<byte[]>();
205 for (var record : account
.getGroupStore().getGroups()) {
206 if (record.isBlocked()) {
207 groupIds
.add(record.getGroupId().serialize());
210 return context
.getSendHelper()
211 .sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
214 public SendMessageResult
sendVerifiedMessage(
215 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
217 var verifiedMessage
= new VerifiedMessage(destination
,
219 trustLevel
.toVerifiedState(),
220 System
.currentTimeMillis());
221 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
224 public SendMessageResult
sendKeysMessage() {
225 var keysMessage
= new KeysMessage(Optional
.ofNullable(account
.getStorageKey()));
226 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forKeys(keysMessage
));
229 public SendMessageResult
sendStickerOperationsMessage(
230 List
<StickerPack
> installStickers
, List
<StickerPack
> removeStickers
232 var installStickerMessages
= installStickers
.stream().map(s
-> getStickerPackOperationMessage(s
, true));
233 var removeStickerMessages
= removeStickers
.stream().map(s
-> getStickerPackOperationMessage(s
, false));
234 var stickerMessages
= Stream
.concat(installStickerMessages
, removeStickerMessages
).toList();
235 return context
.getSendHelper()
236 .sendSyncMessage(SignalServiceSyncMessage
.forStickerPackOperations(stickerMessages
));
239 private static StickerPackOperationMessage
getStickerPackOperationMessage(
240 final StickerPack s
, final boolean installed
242 return new StickerPackOperationMessage(s
.packId().serialize(),
244 installed ? StickerPackOperationMessage
.Type
.INSTALL
: StickerPackOperationMessage
.Type
.REMOVE
);
247 public SendMessageResult
sendConfigurationMessage() {
248 final var config
= account
.getConfigurationStore();
249 var configurationMessage
= new ConfigurationMessage(Optional
.ofNullable(config
.getReadReceipts()),
250 Optional
.ofNullable(config
.getUnidentifiedDeliveryIndicators()),
251 Optional
.ofNullable(config
.getTypingIndicators()),
252 Optional
.ofNullable(config
.getLinkPreviews()));
253 return context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forConfiguration(configurationMessage
));
256 public void handleSyncDeviceGroups(final InputStream input
) {
257 final var s
= new DeviceGroupsInputStream(input
);
262 } catch (IOException e
) {
263 logger
.warn("Sync groups contained invalid group, ignoring: {}", e
.getMessage());
269 var syncGroup
= account
.getGroupStore().getOrCreateGroupV1(GroupId
.v1(g
.getId()));
270 if (syncGroup
!= null) {
271 if (g
.getName().isPresent()) {
272 syncGroup
.name
= g
.getName().get();
274 syncGroup
.addMembers(g
.getMembers()
276 .map(account
.getRecipientResolver()::resolveRecipient
)
277 .collect(Collectors
.toSet()));
279 syncGroup
.removeMember(account
.getSelfRecipientId());
281 // Add ourself to the member set as it's marked as active
282 syncGroup
.addMembers(List
.of(account
.getSelfRecipientId()));
284 syncGroup
.blocked
= g
.isBlocked();
285 if (g
.getColor().isPresent()) {
286 syncGroup
.color
= g
.getColor().get();
289 if (g
.getAvatar().isPresent()) {
290 context
.getGroupHelper().downloadGroupAvatar(syncGroup
.getGroupId(), g
.getAvatar().get());
292 syncGroup
.archived
= g
.isArchived();
293 account
.getGroupStore().updateGroup(syncGroup
);
298 public void handleSyncDeviceContacts(final InputStream input
) throws IOException
{
299 final var s
= new DeviceContactsInputStream(input
);
304 } catch (IOException e
) {
305 if (e
.getMessage() != null && e
.getMessage().contains("Missing contact address!")) {
306 logger
.warn("Sync contacts contained invalid contact, ignoring: {}", e
.getMessage());
315 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
316 account
.setProfileKey(c
.getProfileKey().get());
318 final var recipientId
= account
.getRecipientTrustedResolver().resolveRecipientTrusted(c
.getAddress());
319 var contact
= account
.getContactStore().getContact(recipientId
);
320 final var builder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
321 if (c
.getName().isPresent()) {
322 builder
.withGivenName(c
.getName().get());
323 builder
.withFamilyName(null);
325 if (c
.getColor().isPresent()) {
326 builder
.withColor(c
.getColor().get());
328 if (c
.getProfileKey().isPresent()) {
329 account
.getProfileStore().storeProfileKey(recipientId
, c
.getProfileKey().get());
331 if (c
.getVerified().isPresent()) {
332 final var verifiedMessage
= c
.getVerified().get();
333 account
.getIdentityKeyStore()
334 .setIdentityTrustLevel(verifiedMessage
.getDestination().getServiceId(),
335 verifiedMessage
.getIdentityKey(),
336 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
338 if (c
.getExpirationTimer().isPresent()) {
339 builder
.withMessageExpirationTime(c
.getExpirationTimer().get());
341 builder
.withBlocked(c
.isBlocked());
342 builder
.withArchived(c
.isArchived());
343 account
.getContactStore().storeContact(recipientId
, builder
.build());
345 if (c
.getAvatar().isPresent()) {
346 downloadContactAvatar(c
.getAvatar().get(), new RecipientAddress(c
.getAddress()));
351 private SendMessageResult
requestSyncData(final SyncMessage
.Request
.Type type
) {
352 var r
= new SyncMessage
.Request
.Builder().type(type
).build();
353 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
354 return context
.getSendHelper().sendSyncMessage(message
);
357 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(RecipientAddress address
) throws IOException
{
358 final var streamDetails
= context
.getAvatarStore().retrieveContactAvatar(address
);
359 if (streamDetails
== null) {
360 return Optional
.empty();
363 return Optional
.of(AttachmentUtils
.createAttachmentStream(streamDetails
, Optional
.empty()));
366 private void downloadContactAvatar(SignalServiceAttachment avatar
, RecipientAddress address
) {
368 context
.getAvatarStore()
369 .storeContactAvatar(address
,
370 outputStream
-> context
.getAttachmentHelper().retrieveAttachment(avatar
, outputStream
));
371 } catch (IOException e
) {
372 logger
.warn("Failed to download avatar for contact {}, ignoring: {}", address
, e
.getMessage());