1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.TrustLevel
;
4 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
5 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV1
;
6 import org
.asamk
.signal
.manager
.storage
.recipients
.Contact
;
7 import org
.asamk
.signal
.manager
.util
.AttachmentUtils
;
8 import org
.asamk
.signal
.manager
.util
.IOUtils
;
9 import org
.slf4j
.Logger
;
10 import org
.slf4j
.LoggerFactory
;
11 import org
.whispersystems
.libsignal
.IdentityKey
;
12 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
13 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
14 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
15 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
16 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ConfigurationMessage
;
17 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
18 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
19 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
20 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
21 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
22 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
23 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.KeysMessage
;
24 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
25 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
26 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
27 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
28 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
30 import java
.io
.FileInputStream
;
31 import java
.io
.FileOutputStream
;
32 import java
.io
.IOException
;
33 import java
.io
.InputStream
;
34 import java
.io
.OutputStream
;
35 import java
.nio
.file
.Files
;
36 import java
.util
.ArrayList
;
38 public class SyncHelper
{
40 private final static Logger logger
= LoggerFactory
.getLogger(SyncHelper
.class);
42 private final Context context
;
43 private final SignalAccount account
;
45 public SyncHelper(final Context context
) {
46 this.context
= context
;
47 this.account
= context
.getAccount();
50 public void requestAllSyncData() {
52 requestSyncContacts();
54 requestSyncConfiguration();
58 public void sendSyncFetchProfileMessage() {
59 context
.getSendHelper()
60 .sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.LOCAL_PROFILE
));
63 public void sendGroups() throws IOException
{
64 var groupsFile
= IOUtils
.createTempFile();
67 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
68 var out
= new DeviceGroupsOutputStream(fos
);
69 for (var record : account
.getGroupStore().getGroups()) {
70 if (record instanceof GroupInfoV1 groupInfo
) {
71 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
72 Optional
.fromNullable(groupInfo
.name
),
73 groupInfo
.getMembers()
75 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
77 context
.getGroupHelper().createGroupAvatarAttachment(groupInfo
.getGroupId()),
78 groupInfo
.isMember(account
.getSelfRecipientId()),
79 Optional
.of(groupInfo
.messageExpirationTime
),
80 Optional
.fromNullable(groupInfo
.color
),
88 if (groupsFile
.exists() && groupsFile
.length() > 0) {
89 try (var groupsFileStream
= new FileInputStream(groupsFile
)) {
90 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
91 .withStream(groupsFileStream
)
92 .withContentType("application/octet-stream")
93 .withLength(groupsFile
.length())
96 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
101 Files
.delete(groupsFile
.toPath());
102 } catch (IOException e
) {
103 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
108 public void sendContacts() throws IOException
{
109 var contactsFile
= IOUtils
.createTempFile();
112 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
113 var out
= new DeviceContactsOutputStream(fos
);
114 for (var contactPair
: account
.getContactStore().getContacts()) {
115 final var recipientId
= contactPair
.first();
116 final var contact
= contactPair
.second();
117 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
119 var currentIdentity
= account
.getIdentityKeyStore().getIdentity(recipientId
);
120 VerifiedMessage verifiedMessage
= null;
121 if (currentIdentity
!= null) {
122 verifiedMessage
= new VerifiedMessage(address
,
123 currentIdentity
.getIdentityKey(),
124 currentIdentity
.getTrustLevel().toVerifiedState(),
125 currentIdentity
.getDateAdded().getTime());
128 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
129 out
.write(new DeviceContact(address
,
130 Optional
.fromNullable(contact
.getName()),
131 createContactAvatarAttachment(address
),
132 Optional
.fromNullable(contact
.getColor()),
133 Optional
.fromNullable(verifiedMessage
),
134 Optional
.fromNullable(profileKey
),
136 Optional
.of(contact
.getMessageExpirationTime()),
138 contact
.isArchived()));
141 if (account
.getProfileKey() != null) {
142 // Send our own profile key as well
143 out
.write(new DeviceContact(account
.getSelfAddress(),
148 Optional
.of(account
.getProfileKey()),
156 if (contactsFile
.exists() && contactsFile
.length() > 0) {
157 try (var contactsFileStream
= new FileInputStream(contactsFile
)) {
158 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
159 .withStream(contactsFileStream
)
160 .withContentType("application/octet-stream")
161 .withLength(contactsFile
.length())
164 context
.getSendHelper()
165 .sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
,
171 Files
.delete(contactsFile
.toPath());
172 } catch (IOException e
) {
173 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
178 public void sendBlockedList() {
179 var addresses
= new ArrayList
<SignalServiceAddress
>();
180 for (var record : account
.getContactStore().getContacts()) {
181 if (record.second().isBlocked()) {
182 addresses
.add(context
.getRecipientHelper().resolveSignalServiceAddress(record.first()));
185 var groupIds
= new ArrayList
<byte[]>();
186 for (var record : account
.getGroupStore().getGroups()) {
187 if (record.isBlocked()) {
188 groupIds
.add(record.getGroupId().serialize());
191 context
.getSendHelper()
192 .sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
195 public void sendVerifiedMessage(
196 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
197 ) throws IOException
{
198 var verifiedMessage
= new VerifiedMessage(destination
,
200 trustLevel
.toVerifiedState(),
201 System
.currentTimeMillis());
202 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
205 public void sendKeysMessage() {
206 var keysMessage
= new KeysMessage(Optional
.fromNullable(account
.getStorageKey()));
207 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forKeys(keysMessage
));
210 public void sendConfigurationMessage() {
211 final var config
= account
.getConfigurationStore();
212 var configurationMessage
= new ConfigurationMessage(Optional
.fromNullable(config
.getReadReceipts()),
213 Optional
.fromNullable(config
.getUnidentifiedDeliveryIndicators()),
214 Optional
.fromNullable(config
.getTypingIndicators()),
215 Optional
.fromNullable(config
.getLinkPreviews()));
216 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forConfiguration(configurationMessage
));
219 public void handleSyncDeviceContacts(final InputStream input
) throws IOException
{
220 final var s
= new DeviceContactsInputStream(input
);
225 } catch (IOException e
) {
226 if (e
.getMessage() != null && e
.getMessage().contains("Missing contact address!")) {
227 logger
.warn("Sync contacts contained invalid contact, ignoring: {}", e
.getMessage());
236 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
237 account
.setProfileKey(c
.getProfileKey().get());
239 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(c
.getAddress());
240 var contact
= account
.getContactStore().getContact(recipientId
);
241 final var builder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
242 if (c
.getName().isPresent()) {
243 builder
.withName(c
.getName().get());
245 if (c
.getColor().isPresent()) {
246 builder
.withColor(c
.getColor().get());
248 if (c
.getProfileKey().isPresent()) {
249 account
.getProfileStore().storeProfileKey(recipientId
, c
.getProfileKey().get());
251 if (c
.getVerified().isPresent()) {
252 final var verifiedMessage
= c
.getVerified().get();
253 account
.getIdentityKeyStore()
254 .setIdentityTrustLevel(account
.getRecipientStore()
255 .resolveRecipientTrusted(verifiedMessage
.getDestination()),
256 verifiedMessage
.getIdentityKey(),
257 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
259 if (c
.getExpirationTimer().isPresent()) {
260 builder
.withMessageExpirationTime(c
.getExpirationTimer().get());
262 builder
.withBlocked(c
.isBlocked());
263 builder
.withArchived(c
.isArchived());
264 account
.getContactStore().storeContact(recipientId
, builder
.build());
266 if (c
.getAvatar().isPresent()) {
267 downloadContactAvatar(c
.getAvatar().get(), c
.getAddress());
272 private void requestSyncGroups() {
273 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
274 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
)
276 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
277 context
.getSendHelper().sendSyncMessage(message
);
280 private void requestSyncContacts() {
281 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
282 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
)
284 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
285 context
.getSendHelper().sendSyncMessage(message
);
288 private void requestSyncBlocked() {
289 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
290 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
)
292 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
293 context
.getSendHelper().sendSyncMessage(message
);
296 private void requestSyncConfiguration() {
297 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
298 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
)
300 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
301 context
.getSendHelper().sendSyncMessage(message
);
304 private void requestSyncKeys() {
305 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
306 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.KEYS
)
308 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
309 context
.getSendHelper().sendSyncMessage(message
);
312 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(SignalServiceAddress address
) throws IOException
{
313 final var streamDetails
= context
.getAvatarStore().retrieveContactAvatar(address
);
314 if (streamDetails
== null) {
315 return Optional
.absent();
318 return Optional
.of(AttachmentUtils
.createAttachment(streamDetails
, Optional
.absent()));
321 private void downloadContactAvatar(SignalServiceAttachment avatar
, SignalServiceAddress address
) {
323 context
.getAvatarStore()
324 .storeContactAvatar(address
,
325 outputStream
-> context
.getAttachmentHelper().retrieveAttachment(avatar
, outputStream
));
326 } catch (IOException e
) {
327 logger
.warn("Failed to download avatar for contact {}, ignoring: {}", address
, e
.getMessage());