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
.groups
.GroupId
;
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
.Contact
;
9 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
10 import org
.asamk
.signal
.manager
.util
.AttachmentUtils
;
11 import org
.asamk
.signal
.manager
.util
.IOUtils
;
12 import org
.slf4j
.Logger
;
13 import org
.slf4j
.LoggerFactory
;
14 import org
.whispersystems
.libsignal
.IdentityKey
;
15 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
16 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
17 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
18 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
19 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
20 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
21 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
22 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
23 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
24 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
25 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
26 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
27 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
28 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
29 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
30 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
32 import java
.io
.FileInputStream
;
33 import java
.io
.FileOutputStream
;
34 import java
.io
.IOException
;
35 import java
.io
.InputStream
;
36 import java
.io
.OutputStream
;
37 import java
.nio
.file
.Files
;
38 import java
.util
.ArrayList
;
39 import java
.util
.List
;
40 import java
.util
.stream
.Collectors
;
42 public class SyncHelper
{
44 private final static Logger logger
= LoggerFactory
.getLogger(SyncHelper
.class);
46 private final SignalAccount account
;
47 private final AttachmentHelper attachmentHelper
;
48 private final SendHelper sendHelper
;
49 private final GroupHelper groupHelper
;
50 private final AvatarStore avatarStore
;
51 private final SignalServiceAddressResolver addressResolver
;
52 private final RecipientResolver recipientResolver
;
55 final SignalAccount account
,
56 final AttachmentHelper attachmentHelper
,
57 final SendHelper sendHelper
,
58 final GroupHelper groupHelper
,
59 final AvatarStore avatarStore
,
60 final SignalServiceAddressResolver addressResolver
,
61 final RecipientResolver recipientResolver
63 this.account
= account
;
64 this.attachmentHelper
= attachmentHelper
;
65 this.sendHelper
= sendHelper
;
66 this.groupHelper
= groupHelper
;
67 this.avatarStore
= avatarStore
;
68 this.addressResolver
= addressResolver
;
69 this.recipientResolver
= recipientResolver
;
72 public void requestAllSyncData() throws IOException
{
74 requestSyncContacts();
76 requestSyncConfiguration();
80 public void sendSyncFetchProfileMessage() throws IOException
{
81 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.LOCAL_PROFILE
));
84 public void sendGroups() throws IOException
{
85 var groupsFile
= IOUtils
.createTempFile();
88 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
89 var out
= new DeviceGroupsOutputStream(fos
);
90 for (var record : account
.getGroupStore().getGroups()) {
91 if (record instanceof GroupInfoV1
) {
92 var groupInfo
= (GroupInfoV1
) record;
93 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
94 Optional
.fromNullable(groupInfo
.name
),
95 groupInfo
.getMembers()
97 .map(addressResolver
::resolveSignalServiceAddress
)
98 .collect(Collectors
.toList()),
99 groupHelper
.createGroupAvatarAttachment(groupInfo
.getGroupId()),
100 groupInfo
.isMember(account
.getSelfRecipientId()),
101 Optional
.of(groupInfo
.messageExpirationTime
),
102 Optional
.fromNullable(groupInfo
.color
),
105 groupInfo
.archived
));
110 if (groupsFile
.exists() && groupsFile
.length() > 0) {
111 try (var groupsFileStream
= new FileInputStream(groupsFile
)) {
112 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
113 .withStream(groupsFileStream
)
114 .withContentType("application/octet-stream")
115 .withLength(groupsFile
.length())
118 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
123 Files
.delete(groupsFile
.toPath());
124 } catch (IOException e
) {
125 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
130 public void sendContacts() throws IOException
{
131 var contactsFile
= IOUtils
.createTempFile();
134 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
135 var out
= new DeviceContactsOutputStream(fos
);
136 for (var contactPair
: account
.getContactStore().getContacts()) {
137 final var recipientId
= contactPair
.first();
138 final var contact
= contactPair
.second();
139 final var address
= addressResolver
.resolveSignalServiceAddress(recipientId
);
141 var currentIdentity
= account
.getIdentityKeyStore().getIdentity(recipientId
);
142 VerifiedMessage verifiedMessage
= null;
143 if (currentIdentity
!= null) {
144 verifiedMessage
= new VerifiedMessage(address
,
145 currentIdentity
.getIdentityKey(),
146 currentIdentity
.getTrustLevel().toVerifiedState(),
147 currentIdentity
.getDateAdded().getTime());
150 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
151 out
.write(new DeviceContact(address
,
152 Optional
.fromNullable(contact
.getName()),
153 createContactAvatarAttachment(address
),
154 Optional
.fromNullable(contact
.getColor()),
155 Optional
.fromNullable(verifiedMessage
),
156 Optional
.fromNullable(profileKey
),
158 Optional
.of(contact
.getMessageExpirationTime()),
160 contact
.isArchived()));
163 if (account
.getProfileKey() != null) {
164 // Send our own profile key as well
165 out
.write(new DeviceContact(account
.getSelfAddress(),
170 Optional
.of(account
.getProfileKey()),
178 if (contactsFile
.exists() && contactsFile
.length() > 0) {
179 try (var contactsFileStream
= new FileInputStream(contactsFile
)) {
180 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
181 .withStream(contactsFileStream
)
182 .withContentType("application/octet-stream")
183 .withLength(contactsFile
.length())
186 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
,
192 Files
.delete(contactsFile
.toPath());
193 } catch (IOException e
) {
194 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
199 public void sendBlockedList() throws IOException
{
200 var addresses
= new ArrayList
<SignalServiceAddress
>();
201 for (var record : account
.getContactStore().getContacts()) {
202 if (record.second().isBlocked()) {
203 addresses
.add(addressResolver
.resolveSignalServiceAddress(record.first()));
206 var groupIds
= new ArrayList
<byte[]>();
207 for (var record : account
.getGroupStore().getGroups()) {
208 if (record.isBlocked()) {
209 groupIds
.add(record.getGroupId().serialize());
212 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
215 public void sendVerifiedMessage(
216 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
217 ) throws IOException
{
218 var verifiedMessage
= new VerifiedMessage(destination
,
220 trustLevel
.toVerifiedState(),
221 System
.currentTimeMillis());
222 sendHelper
.sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
225 public void handleSyncDeviceGroups(final InputStream input
) {
226 final var s
= new DeviceGroupsInputStream(input
);
231 } catch (IOException e
) {
232 logger
.warn("Sync groups contained invalid group, ignoring: {}", e
.getMessage());
238 var syncGroup
= account
.getGroupStore().getOrCreateGroupV1(GroupId
.v1(g
.getId()));
239 if (syncGroup
!= null) {
240 if (g
.getName().isPresent()) {
241 syncGroup
.name
= g
.getName().get();
243 syncGroup
.addMembers(g
.getMembers()
245 .map(recipientResolver
::resolveRecipient
)
246 .collect(Collectors
.toSet()));
248 syncGroup
.removeMember(account
.getSelfRecipientId());
250 // Add ourself to the member set as it's marked as active
251 syncGroup
.addMembers(List
.of(account
.getSelfRecipientId()));
253 syncGroup
.blocked
= g
.isBlocked();
254 if (g
.getColor().isPresent()) {
255 syncGroup
.color
= g
.getColor().get();
258 if (g
.getAvatar().isPresent()) {
259 groupHelper
.downloadGroupAvatar(syncGroup
.getGroupId(), g
.getAvatar().get());
261 syncGroup
.archived
= g
.isArchived();
262 account
.getGroupStore().updateGroup(syncGroup
);
267 public void handleSyncDeviceContacts(final InputStream input
) {
268 final var s
= new DeviceContactsInputStream(input
);
273 } catch (IOException e
) {
274 logger
.warn("Sync contacts contained invalid contact, ignoring: {}", e
.getMessage());
280 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
281 account
.setProfileKey(c
.getProfileKey().get());
283 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(c
.getAddress());
284 var contact
= account
.getContactStore().getContact(recipientId
);
285 final var builder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
286 if (c
.getName().isPresent()) {
287 builder
.withName(c
.getName().get());
289 if (c
.getColor().isPresent()) {
290 builder
.withColor(c
.getColor().get());
292 if (c
.getProfileKey().isPresent()) {
293 account
.getProfileStore().storeProfileKey(recipientId
, c
.getProfileKey().get());
295 if (c
.getVerified().isPresent()) {
296 final var verifiedMessage
= c
.getVerified().get();
297 account
.getIdentityKeyStore()
298 .setIdentityTrustLevel(account
.getRecipientStore()
299 .resolveRecipientTrusted(verifiedMessage
.getDestination()),
300 verifiedMessage
.getIdentityKey(),
301 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
303 if (c
.getExpirationTimer().isPresent()) {
304 builder
.withMessageExpirationTime(c
.getExpirationTimer().get());
306 builder
.withBlocked(c
.isBlocked());
307 builder
.withArchived(c
.isArchived());
308 account
.getContactStore().storeContact(recipientId
, builder
.build());
310 if (c
.getAvatar().isPresent()) {
311 downloadContactAvatar(c
.getAvatar().get(), c
.getAddress());
316 private void requestSyncGroups() throws IOException
{
317 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
318 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
)
320 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
321 sendHelper
.sendSyncMessage(message
);
324 private void requestSyncContacts() throws IOException
{
325 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
326 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
)
328 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
329 sendHelper
.sendSyncMessage(message
);
332 private void requestSyncBlocked() throws IOException
{
333 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
334 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
)
336 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
337 sendHelper
.sendSyncMessage(message
);
340 private void requestSyncConfiguration() throws IOException
{
341 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
342 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
)
344 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
345 sendHelper
.sendSyncMessage(message
);
348 private void requestSyncKeys() throws IOException
{
349 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
350 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.KEYS
)
352 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
353 sendHelper
.sendSyncMessage(message
);
356 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(SignalServiceAddress address
) throws IOException
{
357 final var streamDetails
= avatarStore
.retrieveContactAvatar(address
);
358 if (streamDetails
== null) {
359 return Optional
.absent();
362 return Optional
.of(AttachmentUtils
.createAttachment(streamDetails
, Optional
.absent()));
365 private void downloadContactAvatar(SignalServiceAttachment avatar
, SignalServiceAddress address
) {
367 avatarStore
.storeContactAvatar(address
,
368 outputStream
-> attachmentHelper
.retrieveAttachment(avatar
, outputStream
));
369 } catch (IOException e
) {
370 logger
.warn("Failed to download avatar for contact {}, ignoring: {}", address
, e
.getMessage());