1 package org
.asamk
.signal
.manager
.helper
;
3 import com
.google
.protobuf
.ByteString
;
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
.Contact
;
9 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
10 import org
.asamk
.signal
.manager
.util
.AttachmentUtils
;
11 import org
.asamk
.signal
.manager
.util
.IOUtils
;
12 import org
.signal
.libsignal
.protocol
.IdentityKey
;
13 import org
.slf4j
.Logger
;
14 import org
.slf4j
.LoggerFactory
;
15 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
16 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
17 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
18 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ConfigurationMessage
;
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
.DeviceGroupsOutputStream
;
25 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.KeysMessage
;
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
.Optional
;
41 public class SyncHelper
{
43 private final static Logger logger
= LoggerFactory
.getLogger(SyncHelper
.class);
45 private final Context context
;
46 private final SignalAccount account
;
48 public SyncHelper(final Context context
) {
49 this.context
= context
;
50 this.account
= context
.getAccount();
53 public void requestAllSyncData() {
54 requestSyncData(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
);
55 requestSyncData(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
);
56 requestSyncData(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
);
57 requestSyncData(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
);
59 requestSyncPniIdentity();
62 public void requestSyncKeys() {
63 requestSyncData(SignalServiceProtos
.SyncMessage
.Request
.Type
.KEYS
);
66 public void requestSyncPniIdentity() {
67 requestSyncData(SignalServiceProtos
.SyncMessage
.Request
.Type
.PNI_IDENTITY
);
70 public void sendSyncFetchProfileMessage() {
71 context
.getSendHelper()
72 .sendSyncMessage(SignalServiceSyncMessage
.forFetchLatest(SignalServiceSyncMessage
.FetchType
.LOCAL_PROFILE
));
75 public void sendGroups() throws IOException
{
76 var groupsFile
= IOUtils
.createTempFile();
79 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
80 var out
= new DeviceGroupsOutputStream(fos
);
81 for (var record : account
.getGroupStore().getGroups()) {
82 if (record instanceof GroupInfoV1 groupInfo
) {
83 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
84 Optional
.ofNullable(groupInfo
.name
),
85 groupInfo
.getMembers()
87 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
89 context
.getGroupHelper().createGroupAvatarAttachment(groupInfo
.getGroupId()),
90 groupInfo
.isMember(account
.getSelfRecipientId()),
91 Optional
.of(groupInfo
.messageExpirationTime
),
92 Optional
.ofNullable(groupInfo
.color
),
100 if (groupsFile
.exists() && groupsFile
.length() > 0) {
101 try (var groupsFileStream
= new FileInputStream(groupsFile
)) {
102 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
103 .withStream(groupsFileStream
)
104 .withContentType("application/octet-stream")
105 .withLength(groupsFile
.length())
108 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
113 Files
.delete(groupsFile
.toPath());
114 } catch (IOException e
) {
115 logger
.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile
, e
.getMessage());
120 public void sendContacts() throws IOException
{
121 var contactsFile
= IOUtils
.createTempFile();
124 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
125 var out
= new DeviceContactsOutputStream(fos
);
126 for (var contactPair
: account
.getContactStore().getContacts()) {
127 final var recipientId
= contactPair
.first();
128 final var contact
= contactPair
.second();
129 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
131 var currentIdentity
= account
.getIdentityKeyStore().getIdentity(recipientId
);
132 VerifiedMessage verifiedMessage
= null;
133 if (currentIdentity
!= null) {
134 verifiedMessage
= new VerifiedMessage(address
,
135 currentIdentity
.getIdentityKey(),
136 currentIdentity
.getTrustLevel().toVerifiedState(),
137 currentIdentity
.getDateAdded().getTime());
140 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
141 out
.write(new DeviceContact(address
,
142 Optional
.ofNullable(contact
.getName()),
143 createContactAvatarAttachment(new RecipientAddress(address
)),
144 Optional
.ofNullable(contact
.getColor()),
145 Optional
.ofNullable(verifiedMessage
),
146 Optional
.ofNullable(profileKey
),
148 Optional
.of(contact
.getMessageExpirationTime()),
150 contact
.isArchived()));
153 if (account
.getProfileKey() != null) {
154 // Send our own profile key as well
155 out
.write(new DeviceContact(account
.getSelfAddress(),
160 Optional
.of(account
.getProfileKey()),
168 if (contactsFile
.exists() && contactsFile
.length() > 0) {
169 try (var contactsFileStream
= new FileInputStream(contactsFile
)) {
170 var attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
171 .withStream(contactsFileStream
)
172 .withContentType("application/octet-stream")
173 .withLength(contactsFile
.length())
176 context
.getSendHelper()
177 .sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
,
183 Files
.delete(contactsFile
.toPath());
184 } catch (IOException e
) {
185 logger
.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile
, e
.getMessage());
190 public void sendBlockedList() {
191 var addresses
= new ArrayList
<SignalServiceAddress
>();
192 for (var record : account
.getContactStore().getContacts()) {
193 if (record.second().isBlocked()) {
194 addresses
.add(context
.getRecipientHelper().resolveSignalServiceAddress(record.first()));
197 var groupIds
= new ArrayList
<byte[]>();
198 for (var record : account
.getGroupStore().getGroups()) {
199 if (record.isBlocked()) {
200 groupIds
.add(record.getGroupId().serialize());
203 context
.getSendHelper()
204 .sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
207 public void sendVerifiedMessage(
208 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
209 ) throws IOException
{
210 var verifiedMessage
= new VerifiedMessage(destination
,
212 trustLevel
.toVerifiedState(),
213 System
.currentTimeMillis());
214 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
217 public void sendKeysMessage() {
218 var keysMessage
= new KeysMessage(Optional
.ofNullable(account
.getStorageKey()));
219 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forKeys(keysMessage
));
222 public void sendConfigurationMessage() {
223 final var config
= account
.getConfigurationStore();
224 var configurationMessage
= new ConfigurationMessage(Optional
.ofNullable(config
.getReadReceipts()),
225 Optional
.ofNullable(config
.getUnidentifiedDeliveryIndicators()),
226 Optional
.ofNullable(config
.getTypingIndicators()),
227 Optional
.ofNullable(config
.getLinkPreviews()));
228 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forConfiguration(configurationMessage
));
231 public void sendPniIdentity() {
232 final var pniIdentityKeyPair
= account
.getPniIdentityKeyPair();
233 var pniIdentity
= SignalServiceProtos
.SyncMessage
.PniIdentity
.newBuilder()
234 .setPrivateKey(ByteString
.copyFrom(pniIdentityKeyPair
.getPrivateKey().serialize()))
235 .setPublicKey(ByteString
.copyFrom(pniIdentityKeyPair
.getPublicKey().serialize()))
237 context
.getSendHelper().sendSyncMessage(SignalServiceSyncMessage
.forPniIdentity(pniIdentity
));
240 public void handleSyncDeviceContacts(final InputStream input
) throws IOException
{
241 final var s
= new DeviceContactsInputStream(input
);
246 } catch (IOException e
) {
247 if (e
.getMessage() != null && e
.getMessage().contains("Missing contact address!")) {
248 logger
.warn("Sync contacts contained invalid contact, ignoring: {}", e
.getMessage());
257 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
258 account
.setProfileKey(c
.getProfileKey().get());
260 final var recipientId
= account
.getRecipientTrustedResolver().resolveRecipientTrusted(c
.getAddress());
261 var contact
= account
.getContactStore().getContact(recipientId
);
262 final var builder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
263 if (c
.getName().isPresent()) {
264 builder
.withGivenName(c
.getName().get());
266 if (c
.getColor().isPresent()) {
267 builder
.withColor(c
.getColor().get());
269 if (c
.getProfileKey().isPresent()) {
270 account
.getProfileStore().storeProfileKey(recipientId
, c
.getProfileKey().get());
272 if (c
.getVerified().isPresent()) {
273 final var verifiedMessage
= c
.getVerified().get();
274 account
.getIdentityKeyStore()
275 .setIdentityTrustLevel(account
.getRecipientTrustedResolver()
276 .resolveRecipientTrusted(verifiedMessage
.getDestination()),
277 verifiedMessage
.getIdentityKey(),
278 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
280 if (c
.getExpirationTimer().isPresent()) {
281 builder
.withMessageExpirationTime(c
.getExpirationTimer().get());
283 builder
.withBlocked(c
.isBlocked());
284 builder
.withArchived(c
.isArchived());
285 account
.getContactStore().storeContact(recipientId
, builder
.build());
287 if (c
.getAvatar().isPresent()) {
288 downloadContactAvatar(c
.getAvatar().get(), new RecipientAddress(c
.getAddress()));
293 private void requestSyncData(final SignalServiceProtos
.SyncMessage
.Request
.Type type
) {
294 var r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(type
).build();
295 var message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
296 context
.getSendHelper().sendSyncMessage(message
);
299 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(RecipientAddress address
) throws IOException
{
300 final var streamDetails
= context
.getAvatarStore().retrieveContactAvatar(address
);
301 if (streamDetails
== null) {
302 return Optional
.empty();
305 return Optional
.of(AttachmentUtils
.createAttachmentStream(streamDetails
, Optional
.empty()));
308 private void downloadContactAvatar(SignalServiceAttachment avatar
, RecipientAddress address
) {
310 context
.getAvatarStore()
311 .storeContactAvatar(address
,
312 outputStream
-> context
.getAttachmentHelper().retrieveAttachment(avatar
, outputStream
));
313 } catch (IOException e
) {
314 logger
.warn("Failed to download avatar for contact {}, ignoring: {}", address
, e
.getMessage());