1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.AvatarStore
;
4 import org
.asamk
.signal
.manager
.TrustLevel
;
5 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
6 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV1
;
7 import org
.asamk
.signal
.manager
.storage
.recipients
.Contact
;
8 import org
.asamk
.signal
.manager
.util
.AttachmentUtils
;
9 import org
.asamk
.signal
.manager
.util
.IOUtils
;
10 import org
.slf4j
.Logger
;
11 import org
.slf4j
.LoggerFactory
;
12 import org
.whispersystems
.libsignal
.IdentityKey
;
13 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
14 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
15 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
16 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
17 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ConfigurationMessage
;
18 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
19 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
20 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
21 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
22 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
23 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
24 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.KeysMessage
;
25 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
26 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
27 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
28 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
29 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
31 import java
.io
.FileInputStream
;
32 import java
.io
.FileOutputStream
;
33 import java
.io
.IOException
;
34 import java
.io
.InputStream
;
35 import java
.io
.OutputStream
;
36 import java
.nio
.file
.Files
;
37 import java
.util
.ArrayList
;
38 import java
.util
.stream
.Collectors
;
40 public class SyncHelper
{
42 private final static Logger logger
= LoggerFactory
.getLogger(SyncHelper
.class);
44 private final SignalAccount account
;
45 private final AttachmentHelper attachmentHelper
;
46 private final SendHelper sendHelper
;
47 private final GroupHelper groupHelper
;
48 private final AvatarStore avatarStore
;
49 private final SignalServiceAddressResolver addressResolver
;
52 final SignalAccount account
,
53 final AttachmentHelper attachmentHelper
,
54 final SendHelper sendHelper
,
55 final GroupHelper groupHelper
,
56 final AvatarStore avatarStore
,
57 final SignalServiceAddressResolver addressResolver
59 this.account
= account
;
60 this.attachmentHelper
= attachmentHelper
;
61 this.sendHelper
= sendHelper
;
62 this.groupHelper
= groupHelper
;
63 this.avatarStore
= avatarStore
;
64 this.addressResolver
= addressResolver
;
67 public void requestAllSyncData() throws IOException
{
69 requestSyncContacts();
71 requestSyncConfiguration();
75 public void sendSyncFetchProfileMessage() throws IOException
{
76 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.LOCAL_PROFILE
));
79 public void sendGroups() throws IOException
{
80 var groupsFile
= IOUtils
.createTempFile();
83 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
84 var out
= new DeviceGroupsOutputStream(fos
);
85 for (var record : account
.getGroupStore().getGroups()) {
86 if (record instanceof GroupInfoV1
) {
87 var groupInfo
= (GroupInfoV1
) record;
88 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
89 Optional
.fromNullable(groupInfo
.name
),
90 groupInfo
.getMembers()
92 .map(addressResolver
::resolveSignalServiceAddress
)
93 .collect(Collectors
.toList()),
94 groupHelper
.createGroupAvatarAttachment(groupInfo
.getGroupId()),
95 groupInfo
.isMember(account
.getSelfRecipientId()),
96 Optional
.of(groupInfo
.messageExpirationTime
),
97 Optional
.fromNullable(groupInfo
.color
),
100 groupInfo
.archived
));
105 if (groupsFile
.exists() && groupsFile
.length() > 0) {
106 try (var groupsFileStream
= new FileInputStream(groupsFile
)) {
107 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
108 .withStream(groupsFileStream
)
109 .withContentType("application/octet-stream")
110 .withLength(groupsFile
.length())
113 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
118 Files
.delete(groupsFile
.toPath());
119 } catch (IOException e
) {
120 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
125 public void sendContacts() throws IOException
{
126 var contactsFile
= IOUtils
.createTempFile();
129 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
130 var out
= new DeviceContactsOutputStream(fos
);
131 for (var contactPair
: account
.getContactStore().getContacts()) {
132 final var recipientId
= contactPair
.first();
133 final var contact
= contactPair
.second();
134 final var address
= addressResolver
.resolveSignalServiceAddress(recipientId
);
136 var currentIdentity
= account
.getIdentityKeyStore().getIdentity(recipientId
);
137 VerifiedMessage verifiedMessage
= null;
138 if (currentIdentity
!= null) {
139 verifiedMessage
= new VerifiedMessage(address
,
140 currentIdentity
.getIdentityKey(),
141 currentIdentity
.getTrustLevel().toVerifiedState(),
142 currentIdentity
.getDateAdded().getTime());
145 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
146 out
.write(new DeviceContact(address
,
147 Optional
.fromNullable(contact
.getName()),
148 createContactAvatarAttachment(address
),
149 Optional
.fromNullable(contact
.getColor()),
150 Optional
.fromNullable(verifiedMessage
),
151 Optional
.fromNullable(profileKey
),
153 Optional
.of(contact
.getMessageExpirationTime()),
155 contact
.isArchived()));
158 if (account
.getProfileKey() != null) {
159 // Send our own profile key as well
160 out
.write(new DeviceContact(account
.getSelfAddress(),
165 Optional
.of(account
.getProfileKey()),
173 if (contactsFile
.exists() && contactsFile
.length() > 0) {
174 try (var contactsFileStream
= new FileInputStream(contactsFile
)) {
175 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
176 .withStream(contactsFileStream
)
177 .withContentType("application/octet-stream")
178 .withLength(contactsFile
.length())
181 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
,
187 Files
.delete(contactsFile
.toPath());
188 } catch (IOException e
) {
189 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
194 public void sendBlockedList() throws IOException
{
195 var addresses
= new ArrayList
<SignalServiceAddress
>();
196 for (var record : account
.getContactStore().getContacts()) {
197 if (record.second().isBlocked()) {
198 addresses
.add(addressResolver
.resolveSignalServiceAddress(record.first()));
201 var groupIds
= new ArrayList
<byte[]>();
202 for (var record : account
.getGroupStore().getGroups()) {
203 if (record.isBlocked()) {
204 groupIds
.add(record.getGroupId().serialize());
207 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
210 public void sendVerifiedMessage(
211 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
212 ) throws IOException
{
213 var verifiedMessage
= new VerifiedMessage(destination
,
215 trustLevel
.toVerifiedState(),
216 System
.currentTimeMillis());
217 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
220 public void sendKeysMessage() throws IOException
{
221 var keysMessage
= new KeysMessage(Optional
.fromNullable(account
.getStorageKey()));
222 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forKeys(keysMessage
));
225 public void sendConfigurationMessage() throws IOException
{
226 final var config
= account
.getConfigurationStore();
227 var configurationMessage
= new ConfigurationMessage(Optional
.fromNullable(config
.getReadReceipts()),
228 Optional
.fromNullable(config
.getUnidentifiedDeliveryIndicators()),
229 Optional
.fromNullable(config
.getTypingIndicators()),
230 Optional
.fromNullable(config
.getLinkPreviews()));
231 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forConfiguration(configurationMessage
));
234 public void handleSyncDeviceContacts(final InputStream input
) throws IOException
{
235 final var s
= new DeviceContactsInputStream(input
);
240 } catch (IOException e
) {
241 if (e
.getMessage() != null && e
.getMessage().contains("Missing contact address!")) {
242 logger
.warn("Sync contacts contained invalid contact, ignoring: {}", e
.getMessage());
251 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
252 account
.setProfileKey(c
.getProfileKey().get());
254 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(c
.getAddress());
255 var contact
= account
.getContactStore().getContact(recipientId
);
256 final var builder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
257 if (c
.getName().isPresent()) {
258 builder
.withName(c
.getName().get());
260 if (c
.getColor().isPresent()) {
261 builder
.withColor(c
.getColor().get());
263 if (c
.getProfileKey().isPresent()) {
264 account
.getProfileStore().storeProfileKey(recipientId
, c
.getProfileKey().get());
266 if (c
.getVerified().isPresent()) {
267 final var verifiedMessage
= c
.getVerified().get();
268 account
.getIdentityKeyStore()
269 .setIdentityTrustLevel(account
.getRecipientStore()
270 .resolveRecipientTrusted(verifiedMessage
.getDestination()),
271 verifiedMessage
.getIdentityKey(),
272 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
274 if (c
.getExpirationTimer().isPresent()) {
275 builder
.withMessageExpirationTime(c
.getExpirationTimer().get());
277 builder
.withBlocked(c
.isBlocked());
278 builder
.withArchived(c
.isArchived());
279 account
.getContactStore().storeContact(recipientId
, builder
.build());
281 if (c
.getAvatar().isPresent()) {
282 downloadContactAvatar(c
.getAvatar().get(), c
.getAddress());
287 private void requestSyncGroups() throws IOException
{
288 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
289 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
)
291 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
292 sendHelper
.sendSyncMessage(message
);
295 private void requestSyncContacts() throws IOException
{
296 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
297 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
)
299 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
300 sendHelper
.sendSyncMessage(message
);
303 private void requestSyncBlocked() throws IOException
{
304 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
305 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
)
307 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
308 sendHelper
.sendSyncMessage(message
);
311 private void requestSyncConfiguration() throws IOException
{
312 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
313 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
)
315 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
316 sendHelper
.sendSyncMessage(message
);
319 private void requestSyncKeys() throws IOException
{
320 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
321 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.KEYS
)
323 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
324 sendHelper
.sendSyncMessage(message
);
327 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(SignalServiceAddress address
) throws IOException
{
328 final var streamDetails
= avatarStore
.retrieveContactAvatar(address
);
329 if (streamDetails
== null) {
330 return Optional
.absent();
333 return Optional
.of(AttachmentUtils
.createAttachment(streamDetails
, Optional
.absent()));
336 private void downloadContactAvatar(SignalServiceAttachment avatar
, SignalServiceAddress address
) {
338 avatarStore
.storeContactAvatar(address
,
339 outputStream
-> attachmentHelper
.retrieveAttachment(avatar
, outputStream
));
340 } catch (IOException e
) {
341 logger
.warn("Failed to download avatar for contact {}, ignoring: {}", address
, e
.getMessage());