2 Copyright (C) 2015-2021 AsamK and contributors
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 package org
.asamk
.signal
.manager
;
19 import org
.asamk
.signal
.manager
.api
.Configuration
;
20 import org
.asamk
.signal
.manager
.api
.Device
;
21 import org
.asamk
.signal
.manager
.api
.Group
;
22 import org
.asamk
.signal
.manager
.api
.Identity
;
23 import org
.asamk
.signal
.manager
.api
.InactiveGroupLinkException
;
24 import org
.asamk
.signal
.manager
.api
.InvalidDeviceLinkException
;
25 import org
.asamk
.signal
.manager
.api
.Message
;
26 import org
.asamk
.signal
.manager
.api
.Pair
;
27 import org
.asamk
.signal
.manager
.api
.RecipientIdentifier
;
28 import org
.asamk
.signal
.manager
.api
.SendGroupMessageResults
;
29 import org
.asamk
.signal
.manager
.api
.SendMessageResult
;
30 import org
.asamk
.signal
.manager
.api
.SendMessageResults
;
31 import org
.asamk
.signal
.manager
.api
.TypingAction
;
32 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
33 import org
.asamk
.signal
.manager
.api
.UpdateGroup
;
34 import org
.asamk
.signal
.manager
.config
.ServiceEnvironmentConfig
;
35 import org
.asamk
.signal
.manager
.groups
.GroupId
;
36 import org
.asamk
.signal
.manager
.groups
.GroupInviteLinkUrl
;
37 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
38 import org
.asamk
.signal
.manager
.groups
.GroupSendingNotAllowedException
;
39 import org
.asamk
.signal
.manager
.groups
.LastGroupAdminException
;
40 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
41 import org
.asamk
.signal
.manager
.helper
.Context
;
42 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
43 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfo
;
44 import org
.asamk
.signal
.manager
.storage
.identities
.IdentityInfo
;
45 import org
.asamk
.signal
.manager
.storage
.recipients
.Contact
;
46 import org
.asamk
.signal
.manager
.storage
.recipients
.Profile
;
47 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
48 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
49 import org
.asamk
.signal
.manager
.storage
.stickers
.Sticker
;
50 import org
.asamk
.signal
.manager
.storage
.stickers
.StickerPackId
;
51 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
52 import org
.asamk
.signal
.manager
.util
.StickerUtils
;
53 import org
.slf4j
.Logger
;
54 import org
.slf4j
.LoggerFactory
;
55 import org
.whispersystems
.libsignal
.InvalidKeyException
;
56 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
57 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
58 import org
.whispersystems
.signalservice
.api
.SignalSessionLock
;
59 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
60 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
61 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceTypingMessage
;
62 import org
.whispersystems
.signalservice
.api
.push
.ACI
;
63 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.AuthorizationFailedException
;
64 import org
.whispersystems
.signalservice
.api
.util
.DeviceNameUtil
;
65 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
66 import org
.whispersystems
.signalservice
.api
.util
.PhoneNumberFormatter
;
67 import org
.whispersystems
.signalservice
.internal
.util
.DynamicCredentialsProvider
;
68 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
69 import org
.whispersystems
.signalservice
.internal
.util
.Util
;
72 import java
.io
.IOException
;
74 import java
.net
.URISyntaxException
;
75 import java
.net
.URLEncoder
;
76 import java
.nio
.charset
.StandardCharsets
;
77 import java
.time
.Duration
;
78 import java
.util
.ArrayList
;
79 import java
.util
.HashMap
;
80 import java
.util
.HashSet
;
81 import java
.util
.List
;
84 import java
.util
.UUID
;
85 import java
.util
.concurrent
.ExecutorService
;
86 import java
.util
.concurrent
.Executors
;
87 import java
.util
.concurrent
.TimeUnit
;
88 import java
.util
.concurrent
.locks
.ReentrantLock
;
89 import java
.util
.stream
.Collectors
;
90 import java
.util
.stream
.Stream
;
92 import static org
.asamk
.signal
.manager
.config
.ServiceConfig
.capabilities
;
94 public class ManagerImpl
implements Manager
{
96 private final static Logger logger
= LoggerFactory
.getLogger(ManagerImpl
.class);
98 private final SignalDependencies dependencies
;
100 private SignalAccount account
;
102 private final ExecutorService executor
= Executors
.newCachedThreadPool();
104 private final Context context
;
106 private Thread receiveThread
;
107 private final Set
<ReceiveMessageHandler
> weakHandlers
= new HashSet
<>();
108 private final Set
<ReceiveMessageHandler
> messageHandlers
= new HashSet
<>();
109 private final List
<Runnable
> closedListeners
= new ArrayList
<>();
110 private boolean isReceivingSynchronous
;
113 SignalAccount account
,
114 PathConfig pathConfig
,
115 ServiceEnvironmentConfig serviceEnvironmentConfig
,
118 this.account
= account
;
120 final var credentialsProvider
= new DynamicCredentialsProvider(account
.getAci(),
121 account
.getAccount(),
122 account
.getPassword(),
123 account
.getDeviceId());
124 final var sessionLock
= new SignalSessionLock() {
125 private final ReentrantLock LEGACY_LOCK
= new ReentrantLock();
128 public Lock
acquire() {
130 return LEGACY_LOCK
::unlock
;
133 this.dependencies
= new SignalDependencies(serviceEnvironmentConfig
,
136 account
.getSignalProtocolStore(),
139 final var avatarStore
= new AvatarStore(pathConfig
.avatarsPath());
140 final var attachmentStore
= new AttachmentStore(pathConfig
.attachmentsPath());
141 final var stickerPackStore
= new StickerPackStore(pathConfig
.stickerPacksPath());
143 this.context
= new Context(account
, dependencies
, avatarStore
, attachmentStore
, stickerPackStore
);
144 this.context
.getReceiveHelper().setAuthenticationFailureListener(() -> {
147 } catch (IOException e
) {
148 logger
.warn("Failed to close account after authentication failure", e
);
151 this.context
.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> {
152 synchronized (this) {
159 public String
getSelfNumber() {
160 return account
.getAccount();
164 public void checkAccountState() throws IOException
{
165 if (account
.getLastReceiveTimestamp() == 0) {
166 logger
.info("The Signal protocol expects that incoming messages are regularly received.");
168 var diffInMilliseconds
= System
.currentTimeMillis() - account
.getLastReceiveTimestamp();
169 long days
= TimeUnit
.DAYS
.convert(diffInMilliseconds
, TimeUnit
.MILLISECONDS
);
172 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
177 context
.getPreKeyHelper().refreshPreKeysIfNecessary();
178 if (account
.getAci() == null) {
179 account
.setAci(ACI
.parseOrNull(dependencies
.getAccountManager().getWhoAmI().getAci()));
181 updateAccountAttributes(null);
182 } catch (AuthorizationFailedException e
) {
183 account
.setRegistered(false);
189 * This is used for checking a set of phone numbers for registration on Signal
191 * @param numbers The set of phone number in question
192 * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
193 * @throws IOException if it's unable to get the contacts to check if they're registered
196 public Map
<String
, Pair
<String
, UUID
>> areUsersRegistered(Set
<String
> numbers
) throws IOException
{
197 final var canonicalizedNumbers
= numbers
.stream().collect(Collectors
.toMap(n
-> n
, n
-> {
199 final var canonicalizedNumber
= PhoneNumberFormatter
.formatNumber(n
, account
.getAccount());
200 if (!canonicalizedNumber
.equals(n
)) {
201 logger
.debug("Normalized number {} to {}.", n
, canonicalizedNumber
);
203 return canonicalizedNumber
;
204 } catch (InvalidNumberException e
) {
209 // Note "registeredUsers" has no optionals. It only gives us info on users who are registered
210 final var canonicalizedNumbersSet
= canonicalizedNumbers
.values()
212 .filter(s
-> !s
.isEmpty())
213 .collect(Collectors
.toSet());
214 final var registeredUsers
= context
.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet
);
216 return numbers
.stream().collect(Collectors
.toMap(n
-> n
, n
-> {
217 final var number
= canonicalizedNumbers
.get(n
);
218 final var aci
= registeredUsers
.get(number
);
219 return new Pair
<>(number
.isEmpty() ?
null : number
, aci
== null ?
null : aci
.uuid());
224 public void updateAccountAttributes(String deviceName
) throws IOException
{
225 final String encryptedDeviceName
;
226 if (deviceName
== null) {
227 encryptedDeviceName
= account
.getEncryptedDeviceName();
229 final var privateKey
= account
.getIdentityKeyPair().getPrivateKey();
230 encryptedDeviceName
= DeviceNameUtil
.encryptDeviceName(deviceName
, privateKey
);
231 account
.setEncryptedDeviceName(encryptedDeviceName
);
233 dependencies
.getAccountManager()
234 .setAccountAttributes(encryptedDeviceName
,
236 account
.getLocalRegistrationId(),
239 account
.getPinMasterKey() == null ?
null : account
.getPinMasterKey().deriveRegistrationLock(),
240 account
.getSelfUnidentifiedAccessKey(),
241 account
.isUnrestrictedUnidentifiedAccess(),
243 account
.isDiscoverableByPhoneNumber());
247 public Configuration
getConfiguration() {
248 final var configurationStore
= account
.getConfigurationStore();
249 return new Configuration(java
.util
.Optional
.ofNullable(configurationStore
.getReadReceipts()),
250 java
.util
.Optional
.ofNullable(configurationStore
.getUnidentifiedDeliveryIndicators()),
251 java
.util
.Optional
.ofNullable(configurationStore
.getTypingIndicators()),
252 java
.util
.Optional
.ofNullable(configurationStore
.getLinkPreviews()));
256 public void updateConfiguration(
257 Configuration configuration
258 ) throws NotMasterDeviceException
{
259 if (!account
.isMasterDevice()) {
260 throw new NotMasterDeviceException();
263 final var configurationStore
= account
.getConfigurationStore();
264 if (configuration
.readReceipts().isPresent()) {
265 configurationStore
.setReadReceipts(configuration
.readReceipts().get());
267 if (configuration
.unidentifiedDeliveryIndicators().isPresent()) {
268 configurationStore
.setUnidentifiedDeliveryIndicators(configuration
.unidentifiedDeliveryIndicators().get());
270 if (configuration
.typingIndicators().isPresent()) {
271 configurationStore
.setTypingIndicators(configuration
.typingIndicators().get());
273 if (configuration
.linkPreviews().isPresent()) {
274 configurationStore
.setLinkPreviews(configuration
.linkPreviews().get());
276 context
.getSyncHelper().sendConfigurationMessage();
280 * @param givenName if null, the previous givenName will be kept
281 * @param familyName if null, the previous familyName will be kept
282 * @param about if null, the previous about text will be kept
283 * @param aboutEmoji if null, the previous about emoji will be kept
284 * @param avatar if avatar is null the image from the local avatar store is used (if present),
287 public void setProfile(
288 String givenName
, final String familyName
, String about
, String aboutEmoji
, java
.util
.Optional
<File
> avatar
289 ) throws IOException
{
290 context
.getProfileHelper()
291 .setProfile(givenName
,
295 avatar
== null ?
null : Optional
.fromNullable(avatar
.orElse(null)));
296 context
.getSyncHelper().sendSyncFetchProfileMessage();
300 public void unregister() throws IOException
{
301 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
302 // If this is the master device, other users can't send messages to this number anymore.
303 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
304 dependencies
.getAccountManager().setGcmId(Optional
.absent());
306 account
.setRegistered(false);
311 public void deleteAccount() throws IOException
{
313 context
.getPinHelper().removeRegistrationLockPin();
314 } catch (IOException e
) {
315 logger
.warn("Failed to remove registration lock pin");
317 account
.setRegistrationLockPin(null, null);
319 dependencies
.getAccountManager().deleteAccount();
321 account
.setRegistered(false);
326 public void submitRateLimitRecaptchaChallenge(String challenge
, String captcha
) throws IOException
{
327 captcha
= captcha
== null ?
null : captcha
.replace("signalcaptcha://", "");
329 dependencies
.getAccountManager().submitRateLimitRecaptchaChallenge(challenge
, captcha
);
333 public List
<Device
> getLinkedDevices() throws IOException
{
334 var devices
= dependencies
.getAccountManager().getDevices();
335 account
.setMultiDevice(devices
.size() > 1);
336 var identityKey
= account
.getIdentityKeyPair().getPrivateKey();
337 return devices
.stream().map(d
-> {
338 String deviceName
= d
.getName();
339 if (deviceName
!= null) {
341 deviceName
= DeviceNameUtil
.decryptDeviceName(deviceName
, identityKey
);
342 } catch (IOException e
) {
343 logger
.debug("Failed to decrypt device name, maybe plain text?", e
);
346 return new Device(d
.getId(),
350 d
.getId() == account
.getDeviceId());
355 public void removeLinkedDevices(long deviceId
) throws IOException
{
356 dependencies
.getAccountManager().removeDevice(deviceId
);
357 var devices
= dependencies
.getAccountManager().getDevices();
358 account
.setMultiDevice(devices
.size() > 1);
362 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidDeviceLinkException
{
363 var info
= DeviceLinkInfo
.parseDeviceLinkUri(linkUri
);
365 addDevice(info
.deviceIdentifier(), info
.deviceKey());
368 private void addDevice(
369 String deviceIdentifier
, ECPublicKey deviceKey
370 ) throws IOException
, InvalidDeviceLinkException
{
371 var identityKeyPair
= account
.getIdentityKeyPair();
372 var verificationCode
= dependencies
.getAccountManager().getNewDeviceVerificationCode();
375 dependencies
.getAccountManager()
376 .addDevice(deviceIdentifier
,
379 Optional
.of(account
.getProfileKey().serialize()),
381 } catch (InvalidKeyException e
) {
382 throw new InvalidDeviceLinkException("Invalid device link", e
);
384 account
.setMultiDevice(true);
388 public void setRegistrationLockPin(java
.util
.Optional
<String
> pin
) throws IOException
{
389 if (!account
.isMasterDevice()) {
390 throw new RuntimeException("Only master device can set a PIN");
392 if (pin
.isPresent()) {
393 final var masterKey
= account
.getPinMasterKey() != null
394 ? account
.getPinMasterKey()
395 : KeyUtils
.createMasterKey();
397 context
.getPinHelper().setRegistrationLockPin(pin
.get(), masterKey
);
399 account
.setRegistrationLockPin(pin
.get(), masterKey
);
402 context
.getPinHelper().removeRegistrationLockPin();
404 account
.setRegistrationLockPin(null, null);
408 void refreshPreKeys() throws IOException
{
409 context
.getPreKeyHelper().refreshPreKeys();
413 public Profile
getRecipientProfile(RecipientIdentifier
.Single recipient
) throws IOException
, UnregisteredRecipientException
{
414 return context
.getProfileHelper().getRecipientProfile(context
.getRecipientHelper().resolveRecipient(recipient
));
418 public List
<Group
> getGroups() {
419 return account
.getGroupStore().getGroups().stream().map(this::toGroup
).toList();
422 private Group
toGroup(final GroupInfo groupInfo
) {
423 if (groupInfo
== null) {
427 return new Group(groupInfo
.getGroupId(),
428 groupInfo
.getTitle(),
429 groupInfo
.getDescription(),
430 groupInfo
.getGroupInviteLink(),
431 groupInfo
.getMembers()
433 .map(account
.getRecipientStore()::resolveRecipientAddress
)
434 .collect(Collectors
.toSet()),
435 groupInfo
.getPendingMembers()
437 .map(account
.getRecipientStore()::resolveRecipientAddress
)
438 .collect(Collectors
.toSet()),
439 groupInfo
.getRequestingMembers()
441 .map(account
.getRecipientStore()::resolveRecipientAddress
)
442 .collect(Collectors
.toSet()),
443 groupInfo
.getAdminMembers()
445 .map(account
.getRecipientStore()::resolveRecipientAddress
)
446 .collect(Collectors
.toSet()),
447 groupInfo
.isBlocked(),
448 groupInfo
.getMessageExpirationTimer(),
449 groupInfo
.getPermissionAddMember(),
450 groupInfo
.getPermissionEditDetails(),
451 groupInfo
.getPermissionSendMessage(),
452 groupInfo
.isMember(account
.getSelfRecipientId()),
453 groupInfo
.isAdmin(account
.getSelfRecipientId()));
457 public SendGroupMessageResults
quitGroup(
458 GroupId groupId
, Set
<RecipientIdentifier
.Single
> groupAdmins
459 ) throws GroupNotFoundException
, IOException
, NotAGroupMemberException
, LastGroupAdminException
, UnregisteredRecipientException
{
460 final var newAdmins
= context
.getRecipientHelper().resolveRecipients(groupAdmins
);
461 return context
.getGroupHelper().quitGroup(groupId
, newAdmins
);
465 public void deleteGroup(GroupId groupId
) throws IOException
{
466 context
.getGroupHelper().deleteGroup(groupId
);
470 public Pair
<GroupId
, SendGroupMessageResults
> createGroup(
471 String name
, Set
<RecipientIdentifier
.Single
> members
, File avatarFile
472 ) throws IOException
, AttachmentInvalidException
, UnregisteredRecipientException
{
473 return context
.getGroupHelper()
475 members
== null ?
null : context
.getRecipientHelper().resolveRecipients(members
),
480 public SendGroupMessageResults
updateGroup(
481 final GroupId groupId
, final UpdateGroup updateGroup
482 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
, GroupSendingNotAllowedException
, UnregisteredRecipientException
{
483 return context
.getGroupHelper()
484 .updateGroup(groupId
,
485 updateGroup
.getName(),
486 updateGroup
.getDescription(),
487 updateGroup
.getMembers() == null
489 : context
.getRecipientHelper().resolveRecipients(updateGroup
.getMembers()),
490 updateGroup
.getRemoveMembers() == null
492 : context
.getRecipientHelper().resolveRecipients(updateGroup
.getRemoveMembers()),
493 updateGroup
.getAdmins() == null
495 : context
.getRecipientHelper().resolveRecipients(updateGroup
.getAdmins()),
496 updateGroup
.getRemoveAdmins() == null
498 : context
.getRecipientHelper().resolveRecipients(updateGroup
.getRemoveAdmins()),
499 updateGroup
.isResetGroupLink(),
500 updateGroup
.getGroupLinkState(),
501 updateGroup
.getAddMemberPermission(),
502 updateGroup
.getEditDetailsPermission(),
503 updateGroup
.getAvatarFile(),
504 updateGroup
.getExpirationTimer(),
505 updateGroup
.getIsAnnouncementGroup());
509 public Pair
<GroupId
, SendGroupMessageResults
> joinGroup(
510 GroupInviteLinkUrl inviteLinkUrl
511 ) throws IOException
, InactiveGroupLinkException
{
512 return context
.getGroupHelper().joinGroup(inviteLinkUrl
);
515 private SendMessageResults
sendMessage(
516 SignalServiceDataMessage
.Builder messageBuilder
, Set
<RecipientIdentifier
> recipients
517 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
518 var results
= new HashMap
<RecipientIdentifier
, List
<SendMessageResult
>>();
519 long timestamp
= System
.currentTimeMillis();
520 messageBuilder
.withTimestamp(timestamp
);
521 for (final var recipient
: recipients
) {
522 if (recipient
instanceof RecipientIdentifier
.Single single
) {
524 final var recipientId
= context
.getRecipientHelper().resolveRecipient(single
);
525 final var result
= context
.getSendHelper().sendMessage(messageBuilder
, recipientId
);
526 results
.put(recipient
,
527 List
.of(SendMessageResult
.from(result
,
528 account
.getRecipientStore(),
529 account
.getRecipientStore()::resolveRecipientAddress
)));
530 } catch (UnregisteredRecipientException e
) {
531 results
.put(recipient
,
532 List
.of(SendMessageResult
.unregisteredFailure(single
.toPartialRecipientAddress())));
534 } else if (recipient
instanceof RecipientIdentifier
.NoteToSelf
) {
535 final var result
= context
.getSendHelper().sendSelfMessage(messageBuilder
);
536 results
.put(recipient
,
537 List
.of(SendMessageResult
.from(result
,
538 account
.getRecipientStore(),
539 account
.getRecipientStore()::resolveRecipientAddress
)));
540 } else if (recipient
instanceof RecipientIdentifier
.Group group
) {
541 final var result
= context
.getSendHelper().sendAsGroupMessage(messageBuilder
, group
.groupId());
542 results
.put(recipient
,
544 .map(sendMessageResult
-> SendMessageResult
.from(sendMessageResult
,
545 account
.getRecipientStore(),
546 account
.getRecipientStore()::resolveRecipientAddress
))
550 return new SendMessageResults(timestamp
, results
);
553 private SendMessageResults
sendTypingMessage(
554 SignalServiceTypingMessage
.Action action
, Set
<RecipientIdentifier
> recipients
555 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
556 var results
= new HashMap
<RecipientIdentifier
, List
<SendMessageResult
>>();
557 final var timestamp
= System
.currentTimeMillis();
558 for (var recipient
: recipients
) {
559 if (recipient
instanceof RecipientIdentifier
.Single single
) {
560 final var message
= new SignalServiceTypingMessage(action
, timestamp
, Optional
.absent());
562 final var recipientId
= context
.getRecipientHelper().resolveRecipient(single
);
563 final var result
= context
.getSendHelper().sendTypingMessage(message
, recipientId
);
564 results
.put(recipient
,
565 List
.of(SendMessageResult
.from(result
,
566 account
.getRecipientStore(),
567 account
.getRecipientStore()::resolveRecipientAddress
)));
568 } catch (UnregisteredRecipientException e
) {
569 results
.put(recipient
,
570 List
.of(SendMessageResult
.unregisteredFailure(single
.toPartialRecipientAddress())));
572 } else if (recipient
instanceof RecipientIdentifier
.Group
) {
573 final var groupId
= ((RecipientIdentifier
.Group
) recipient
).groupId();
574 final var message
= new SignalServiceTypingMessage(action
, timestamp
, Optional
.of(groupId
.serialize()));
575 final var result
= context
.getSendHelper().sendGroupTypingMessage(message
, groupId
);
576 results
.put(recipient
,
578 .map(r
-> SendMessageResult
.from(r
,
579 account
.getRecipientStore(),
580 account
.getRecipientStore()::resolveRecipientAddress
))
584 return new SendMessageResults(timestamp
, results
);
588 public SendMessageResults
sendTypingMessage(
589 TypingAction action
, Set
<RecipientIdentifier
> recipients
590 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
591 return sendTypingMessage(action
.toSignalService(), recipients
);
595 public SendMessageResults
sendReadReceipt(
596 RecipientIdentifier
.Single sender
, List
<Long
> messageIds
597 ) throws IOException
{
598 final var timestamp
= System
.currentTimeMillis();
599 var receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.READ
,
603 return sendReceiptMessage(sender
, timestamp
, receiptMessage
);
607 public SendMessageResults
sendViewedReceipt(
608 RecipientIdentifier
.Single sender
, List
<Long
> messageIds
609 ) throws IOException
{
610 final var timestamp
= System
.currentTimeMillis();
611 var receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.VIEWED
,
615 return sendReceiptMessage(sender
, timestamp
, receiptMessage
);
618 private SendMessageResults
sendReceiptMessage(
619 final RecipientIdentifier
.Single sender
,
620 final long timestamp
,
621 final SignalServiceReceiptMessage receiptMessage
622 ) throws IOException
{
624 final var result
= context
.getSendHelper()
625 .sendReceiptMessage(receiptMessage
, context
.getRecipientHelper().resolveRecipient(sender
));
626 return new SendMessageResults(timestamp
,
628 List
.of(SendMessageResult
.from(result
,
629 account
.getRecipientStore(),
630 account
.getRecipientStore()::resolveRecipientAddress
))));
631 } catch (UnregisteredRecipientException e
) {
632 return new SendMessageResults(timestamp
,
633 Map
.of(sender
, List
.of(SendMessageResult
.unregisteredFailure(sender
.toPartialRecipientAddress()))));
638 public SendMessageResults
sendMessage(
639 Message message
, Set
<RecipientIdentifier
> recipients
640 ) throws IOException
, AttachmentInvalidException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
, UnregisteredRecipientException
{
641 final var messageBuilder
= SignalServiceDataMessage
.newBuilder();
642 applyMessage(messageBuilder
, message
);
643 return sendMessage(messageBuilder
, recipients
);
646 private void applyMessage(
647 final SignalServiceDataMessage
.Builder messageBuilder
, final Message message
648 ) throws AttachmentInvalidException
, IOException
, UnregisteredRecipientException
{
649 messageBuilder
.withBody(message
.messageText());
650 final var attachments
= message
.attachments();
651 if (attachments
!= null) {
652 messageBuilder
.withAttachments(context
.getAttachmentHelper().uploadAttachments(attachments
));
654 if (message
.mentions().size() > 0) {
655 messageBuilder
.withMentions(resolveMentions(message
.mentions()));
657 if (message
.quote().isPresent()) {
658 final var quote
= message
.quote().get();
659 messageBuilder
.withQuote(new SignalServiceDataMessage
.Quote(quote
.timestamp(),
660 context
.getRecipientHelper()
661 .resolveSignalServiceAddress(context
.getRecipientHelper().resolveRecipient(quote
.author())),
664 resolveMentions(quote
.mentions())));
668 private ArrayList
<SignalServiceDataMessage
.Mention
> resolveMentions(final List
<Message
.Mention
> mentionList
) throws IOException
, UnregisteredRecipientException
{
669 final var mentions
= new ArrayList
<SignalServiceDataMessage
.Mention
>();
670 for (final var m
: mentionList
) {
671 final var recipientId
= context
.getRecipientHelper().resolveRecipient(m
.recipient());
672 mentions
.add(new SignalServiceDataMessage
.Mention(context
.getRecipientHelper()
673 .resolveSignalServiceAddress(recipientId
)
674 .getAci(), m
.start(), m
.length()));
680 public SendMessageResults
sendRemoteDeleteMessage(
681 long targetSentTimestamp
, Set
<RecipientIdentifier
> recipients
682 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
683 var delete
= new SignalServiceDataMessage
.RemoteDelete(targetSentTimestamp
);
684 final var messageBuilder
= SignalServiceDataMessage
.newBuilder().withRemoteDelete(delete
);
685 return sendMessage(messageBuilder
, recipients
);
689 public SendMessageResults
sendMessageReaction(
692 RecipientIdentifier
.Single targetAuthor
,
693 long targetSentTimestamp
,
694 Set
<RecipientIdentifier
> recipients
695 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
, UnregisteredRecipientException
{
696 var targetAuthorRecipientId
= context
.getRecipientHelper().resolveRecipient(targetAuthor
);
697 var reaction
= new SignalServiceDataMessage
.Reaction(emoji
,
699 context
.getRecipientHelper().resolveSignalServiceAddress(targetAuthorRecipientId
),
700 targetSentTimestamp
);
701 final var messageBuilder
= SignalServiceDataMessage
.newBuilder().withReaction(reaction
);
702 return sendMessage(messageBuilder
, recipients
);
706 public SendMessageResults
sendEndSessionMessage(Set
<RecipientIdentifier
.Single
> recipients
) throws IOException
{
707 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asEndSessionMessage();
710 return sendMessage(messageBuilder
,
711 recipients
.stream().map(RecipientIdentifier
.class::cast
).collect(Collectors
.toSet()));
712 } catch (GroupNotFoundException
| NotAGroupMemberException
| GroupSendingNotAllowedException e
) {
713 throw new AssertionError(e
);
715 for (var recipient
: recipients
) {
716 final RecipientId recipientId
;
718 recipientId
= context
.getRecipientHelper().resolveRecipient(recipient
);
719 } catch (UnregisteredRecipientException e
) {
722 account
.getSessionStore().deleteAllSessions(recipientId
);
728 public void deleteRecipient(final RecipientIdentifier
.Single recipient
) {
729 account
.removeRecipient(account
.getRecipientStore().resolveRecipient(recipient
.toPartialRecipientAddress()));
733 public void deleteContact(final RecipientIdentifier
.Single recipient
) {
734 account
.getContactStore()
735 .deleteContact(account
.getRecipientStore().resolveRecipient(recipient
.toPartialRecipientAddress()));
739 public void setContactName(
740 RecipientIdentifier
.Single recipient
, String name
741 ) throws NotMasterDeviceException
, IOException
, UnregisteredRecipientException
{
742 if (!account
.isMasterDevice()) {
743 throw new NotMasterDeviceException();
745 context
.getContactHelper().setContactName(context
.getRecipientHelper().resolveRecipient(recipient
), name
);
749 public void setContactBlocked(
750 RecipientIdentifier
.Single recipient
, boolean blocked
751 ) throws NotMasterDeviceException
, IOException
, UnregisteredRecipientException
{
752 if (!account
.isMasterDevice()) {
753 throw new NotMasterDeviceException();
755 context
.getContactHelper().setContactBlocked(context
.getRecipientHelper().resolveRecipient(recipient
), blocked
);
756 // TODO cycle our profile key, if we're not together in a group with recipient
757 context
.getSyncHelper().sendBlockedList();
761 public void setGroupBlocked(
762 final GroupId groupId
, final boolean blocked
763 ) throws GroupNotFoundException
, NotMasterDeviceException
{
764 if (!account
.isMasterDevice()) {
765 throw new NotMasterDeviceException();
767 context
.getGroupHelper().setGroupBlocked(groupId
, blocked
);
768 // TODO cycle our profile key
769 context
.getSyncHelper().sendBlockedList();
773 * Change the expiration timer for a contact
776 public void setExpirationTimer(
777 RecipientIdentifier
.Single recipient
, int messageExpirationTimer
778 ) throws IOException
, UnregisteredRecipientException
{
779 var recipientId
= context
.getRecipientHelper().resolveRecipient(recipient
);
780 context
.getContactHelper().setExpirationTimer(recipientId
, messageExpirationTimer
);
781 final var messageBuilder
= SignalServiceDataMessage
.newBuilder().asExpirationUpdate();
783 sendMessage(messageBuilder
, Set
.of(recipient
));
784 } catch (NotAGroupMemberException
| GroupNotFoundException
| GroupSendingNotAllowedException e
) {
785 throw new AssertionError(e
);
790 * Upload the sticker pack from path.
792 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
793 * @return if successful, returns the URL to install the sticker pack in the signal app
796 public URI
uploadStickerPack(File path
) throws IOException
, StickerPackInvalidException
{
797 var manifest
= StickerUtils
.getSignalServiceStickerManifestUpload(path
);
799 var messageSender
= dependencies
.getMessageSender();
801 var packKey
= KeyUtils
.createStickerUploadKey();
802 var packIdString
= messageSender
.uploadStickerManifest(manifest
, packKey
);
803 var packId
= StickerPackId
.deserialize(Hex
.fromStringCondensed(packIdString
));
805 var sticker
= new Sticker(packId
, packKey
);
806 account
.getStickerStore().updateSticker(sticker
);
809 return new URI("https",
813 + URLEncoder
.encode(Hex
.toStringCondensed(packId
.serialize()), StandardCharsets
.UTF_8
)
815 + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), StandardCharsets
.UTF_8
));
816 } catch (URISyntaxException e
) {
817 throw new AssertionError(e
);
822 public void requestAllSyncData() throws IOException
{
823 context
.getSyncHelper().requestAllSyncData();
824 retrieveRemoteStorage();
827 void retrieveRemoteStorage() throws IOException
{
828 if (account
.getStorageKey() != null) {
829 context
.getStorageHelper().readDataFromStorage();
834 public void addReceiveHandler(final ReceiveMessageHandler handler
, final boolean isWeakListener
) {
835 if (isReceivingSynchronous
) {
836 throw new IllegalStateException("Already receiving message synchronously.");
838 synchronized (messageHandlers
) {
839 if (isWeakListener
) {
840 weakHandlers
.add(handler
);
842 messageHandlers
.add(handler
);
843 startReceiveThreadIfRequired();
848 private void startReceiveThreadIfRequired() {
849 if (receiveThread
!= null) {
852 receiveThread
= new Thread(() -> {
853 logger
.debug("Starting receiving messages");
854 while (!Thread
.interrupted()) {
856 context
.getReceiveHelper().receiveMessages(Duration
.ofMinutes(1), false, (envelope
, e
) -> {
857 synchronized (messageHandlers
) {
858 Stream
.concat(messageHandlers
.stream(), weakHandlers
.stream()).forEach(h
-> {
860 h
.handleMessage(envelope
, e
);
861 } catch (Exception ex
) {
862 logger
.warn("Message handler failed, ignoring", ex
);
868 } catch (IOException e
) {
869 logger
.warn("Receiving messages failed, retrying", e
);
872 logger
.debug("Finished receiving messages");
873 synchronized (messageHandlers
) {
874 receiveThread
= null;
876 // Check if in the meantime another handler has been registered
877 if (!messageHandlers
.isEmpty()) {
878 logger
.debug("Another handler has been registered, starting receive thread again");
879 startReceiveThreadIfRequired();
884 receiveThread
.start();
888 public void removeReceiveHandler(final ReceiveMessageHandler handler
) {
890 synchronized (messageHandlers
) {
891 weakHandlers
.remove(handler
);
892 messageHandlers
.remove(handler
);
893 if (!messageHandlers
.isEmpty() || receiveThread
== null || isReceivingSynchronous
) {
896 thread
= receiveThread
;
897 receiveThread
= null;
900 stopReceiveThread(thread
);
903 private void stopReceiveThread(final Thread thread
) {
907 } catch (InterruptedException ignored
) {
912 public boolean isReceiving() {
913 if (isReceivingSynchronous
) {
916 synchronized (messageHandlers
) {
917 return messageHandlers
.size() > 0;
922 public void receiveMessages(Duration timeout
, ReceiveMessageHandler handler
) throws IOException
{
923 receiveMessages(timeout
, true, handler
);
927 public void receiveMessages(ReceiveMessageHandler handler
) throws IOException
{
928 receiveMessages(Duration
.ofMinutes(1), false, handler
);
931 private void receiveMessages(
932 Duration timeout
, boolean returnOnTimeout
, ReceiveMessageHandler handler
933 ) throws IOException
{
935 throw new IllegalStateException("Already receiving message.");
937 isReceivingSynchronous
= true;
938 receiveThread
= Thread
.currentThread();
940 context
.getReceiveHelper().receiveMessages(timeout
, returnOnTimeout
, handler
);
942 receiveThread
= null;
943 isReceivingSynchronous
= false;
948 public void setIgnoreAttachments(final boolean ignoreAttachments
) {
949 context
.getReceiveHelper().setIgnoreAttachments(ignoreAttachments
);
953 public boolean hasCaughtUpWithOldMessages() {
954 return context
.getReceiveHelper().hasCaughtUpWithOldMessages();
958 public boolean isContactBlocked(final RecipientIdentifier
.Single recipient
) {
959 final RecipientId recipientId
;
961 recipientId
= context
.getRecipientHelper().resolveRecipient(recipient
);
962 } catch (IOException
| UnregisteredRecipientException e
) {
965 return context
.getContactHelper().isContactBlocked(recipientId
);
969 public void sendContacts() throws IOException
{
970 context
.getSyncHelper().sendContacts();
974 public List
<Pair
<RecipientAddress
, Contact
>> getContacts() {
975 return account
.getContactStore()
978 .map(p
-> new Pair
<>(account
.getRecipientStore().resolveRecipientAddress(p
.first()), p
.second()))
983 public String
getContactOrProfileName(RecipientIdentifier
.Single recipient
) {
984 final RecipientId recipientId
;
986 recipientId
= context
.getRecipientHelper().resolveRecipient(recipient
);
987 } catch (IOException
| UnregisteredRecipientException e
) {
991 final var contact
= account
.getContactStore().getContact(recipientId
);
992 if (contact
!= null && !Util
.isEmpty(contact
.getName())) {
993 return contact
.getName();
996 final var profile
= context
.getProfileHelper().getRecipientProfile(recipientId
);
997 if (profile
!= null) {
998 return profile
.getDisplayName();
1005 public Group
getGroup(GroupId groupId
) {
1006 return toGroup(context
.getGroupHelper().getGroup(groupId
));
1010 public List
<Identity
> getIdentities() {
1011 return account
.getIdentityKeyStore().getIdentities().stream().map(this::toIdentity
).toList();
1014 private Identity
toIdentity(final IdentityInfo identityInfo
) {
1015 if (identityInfo
== null) {
1019 final var address
= account
.getRecipientStore().resolveRecipientAddress(identityInfo
.getRecipientId());
1020 final var scannableFingerprint
= context
.getIdentityHelper()
1021 .computeSafetyNumberForScanning(identityInfo
.getRecipientId(), identityInfo
.getIdentityKey());
1022 return new Identity(address
,
1023 identityInfo
.getIdentityKey(),
1024 context
.getIdentityHelper()
1025 .computeSafetyNumber(identityInfo
.getRecipientId(), identityInfo
.getIdentityKey()),
1026 scannableFingerprint
== null ?
null : scannableFingerprint
.getSerialized(),
1027 identityInfo
.getTrustLevel(),
1028 identityInfo
.getDateAdded());
1032 public List
<Identity
> getIdentities(RecipientIdentifier
.Single recipient
) {
1033 IdentityInfo identity
;
1035 identity
= account
.getIdentityKeyStore()
1036 .getIdentity(context
.getRecipientHelper().resolveRecipient(recipient
));
1037 } catch (IOException
| UnregisteredRecipientException e
) {
1040 return identity
== null ? List
.of() : List
.of(toIdentity(identity
));
1044 * Trust this the identity with this fingerprint
1046 * @param recipient account of the identity
1047 * @param fingerprint Fingerprint
1050 public boolean trustIdentityVerified(
1051 RecipientIdentifier
.Single recipient
, byte[] fingerprint
1052 ) throws UnregisteredRecipientException
{
1053 RecipientId recipientId
;
1055 recipientId
= context
.getRecipientHelper().resolveRecipient(recipient
);
1056 } catch (IOException e
) {
1059 final var updated
= context
.getIdentityHelper().trustIdentityVerified(recipientId
, fingerprint
);
1060 if (updated
&& this.isReceiving()) {
1061 context
.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1067 * Trust this the identity with this safety number
1069 * @param recipient account of the identity
1070 * @param safetyNumber Safety number
1073 public boolean trustIdentityVerifiedSafetyNumber(
1074 RecipientIdentifier
.Single recipient
, String safetyNumber
1075 ) throws UnregisteredRecipientException
{
1076 RecipientId recipientId
;
1078 recipientId
= context
.getRecipientHelper().resolveRecipient(recipient
);
1079 } catch (IOException e
) {
1082 final var updated
= context
.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId
, safetyNumber
);
1083 if (updated
&& this.isReceiving()) {
1084 context
.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1090 * Trust this the identity with this scannable safety number
1092 * @param recipient account of the identity
1093 * @param safetyNumber Scannable safety number
1096 public boolean trustIdentityVerifiedSafetyNumber(
1097 RecipientIdentifier
.Single recipient
, byte[] safetyNumber
1098 ) throws UnregisteredRecipientException
{
1099 RecipientId recipientId
;
1101 recipientId
= context
.getRecipientHelper().resolveRecipient(recipient
);
1102 } catch (IOException e
) {
1105 final var updated
= context
.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId
, safetyNumber
);
1106 if (updated
&& this.isReceiving()) {
1107 context
.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1113 * Trust all keys of this identity without verification
1115 * @param recipient account of the identity
1118 public boolean trustIdentityAllKeys(RecipientIdentifier
.Single recipient
) throws UnregisteredRecipientException
{
1119 RecipientId recipientId
;
1121 recipientId
= context
.getRecipientHelper().resolveRecipient(recipient
);
1122 } catch (IOException e
) {
1125 final var updated
= context
.getIdentityHelper().trustIdentityAllKeys(recipientId
);
1126 if (updated
&& this.isReceiving()) {
1127 context
.getReceiveHelper().setNeedsToRetryFailedMessages(true);
1133 public void addClosedListener(final Runnable listener
) {
1134 synchronized (closedListeners
) {
1135 closedListeners
.add(listener
);
1140 public void close() throws IOException
{
1142 synchronized (messageHandlers
) {
1143 weakHandlers
.clear();
1144 messageHandlers
.clear();
1145 thread
= receiveThread
;
1146 receiveThread
= null;
1148 if (thread
!= null) {
1149 stopReceiveThread(thread
);
1151 executor
.shutdown();
1153 dependencies
.getSignalWebSocket().disconnect();
1155 synchronized (closedListeners
) {
1156 closedListeners
.forEach(Runnable
::run
);
1157 closedListeners
.clear();
1160 if (account
!= null) {