package org.asamk.signal.manager;
import org.asamk.signal.manager.actions.HandleAction;
+import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendGroupMessageResults;
+import org.asamk.signal.manager.api.SendMessageResult;
import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UpdateGroup;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalSessionLock;
-import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
+import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SignatureException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
private boolean ignoreAttachments = false;
private Thread receiveThread;
+ private final Set<ReceiveMessageHandler> weakHandlers = new HashSet<>();
private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
+ private final List<Runnable> closedListeners = new ArrayList<>();
private boolean isReceivingSynchronous;
ManagerImpl(
this.account = account;
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
- final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(),
- account.getUsername(),
+ final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(),
+ account.getAccount(),
account.getPassword(),
account.getDeviceId());
final var sessionLock = new SignalSessionLock() {
@Override
public String getSelfNumber() {
- return account.getUsername();
+ return account.getAccount();
}
@Override
}
}
preKeyHelper.refreshPreKeysIfNecessary();
- if (account.getUuid() == null) {
- account.setUuid(dependencies.getAccountManager().getOwnUuid());
+ if (account.getAci() == null) {
+ account.setAci(dependencies.getAccountManager().getOwnAci());
}
updateAccountAttributes(null);
}
*
* @param numbers The set of phone number in question
* @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
- * @throws IOException if its unable to get the contacts to check if they're registered
+ * @throws IOException if it's unable to get the contacts to check if they're registered
*/
@Override
public Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) throws IOException {
Map<String, String> canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
try {
- return PhoneNumberFormatter.formatNumber(n, account.getUsername());
+ final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getAccount());
+ if (!canonicalizedNumber.equals(n)) {
+ logger.debug("Normalized number {} to {}.", n, canonicalizedNumber);
+ }
+ return canonicalizedNumber;
} catch (InvalidNumberException e) {
return "";
}
return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
final var number = canonicalizedNumbers.get(n);
- final var uuid = registeredUsers.get(number);
- return new Pair<>(number.isEmpty() ? null : number, uuid);
+ final var aci = registeredUsers.get(number);
+ return new Pair<>(number.isEmpty() ? null : number, aci == null ? null : aci.uuid());
}));
}
account.isDiscoverableByPhoneNumber());
}
+ @Override
+ public Configuration getConfiguration() {
+ final var configurationStore = account.getConfigurationStore();
+ return new Configuration(java.util.Optional.ofNullable(configurationStore.getReadReceipts()),
+ java.util.Optional.ofNullable(configurationStore.getUnidentifiedDeliveryIndicators()),
+ java.util.Optional.ofNullable(configurationStore.getTypingIndicators()),
+ java.util.Optional.ofNullable(configurationStore.getLinkPreviews()));
+ }
+
@Override
public void updateConfiguration(
- final Boolean readReceipts,
- final Boolean unidentifiedDeliveryIndicators,
- final Boolean typingIndicators,
- final Boolean linkPreviews
+ Configuration configuration
) throws IOException, NotMasterDeviceException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
final var configurationStore = account.getConfigurationStore();
- if (readReceipts != null) {
- configurationStore.setReadReceipts(readReceipts);
+ if (configuration.readReceipts().isPresent()) {
+ configurationStore.setReadReceipts(configuration.readReceipts().get());
}
- if (unidentifiedDeliveryIndicators != null) {
- configurationStore.setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators);
+ if (configuration.unidentifiedDeliveryIndicators().isPresent()) {
+ configurationStore.setUnidentifiedDeliveryIndicators(configuration.unidentifiedDeliveryIndicators().get());
}
- if (typingIndicators != null) {
- configurationStore.setTypingIndicators(typingIndicators);
+ if (configuration.typingIndicators().isPresent()) {
+ configurationStore.setTypingIndicators(configuration.typingIndicators().get());
}
- if (linkPreviews != null) {
- configurationStore.setLinkPreviews(linkPreviews);
+ if (configuration.linkPreviews().isPresent()) {
+ configurationStore.setLinkPreviews(configuration.linkPreviews().get());
}
syncHelper.sendConfigurationMessage();
}
dependencies.getAccountManager().setGcmId(Optional.absent());
account.setRegistered(false);
+ close();
}
@Override
dependencies.getAccountManager().deleteAccount();
account.setRegistered(false);
+ close();
}
@Override
public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
+ captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
+
dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
}
if (recipient instanceof RecipientIdentifier.Single single) {
final var recipientId = resolveRecipient(single);
final var result = sendHelper.sendMessage(messageBuilder, recipientId);
- results.put(recipient, List.of(result));
+ results.put(recipient,
+ List.of(SendMessageResult.from(result,
+ account.getRecipientStore(),
+ account.getRecipientStore()::resolveRecipientAddress)));
} else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
final var result = sendHelper.sendSelfMessage(messageBuilder);
- results.put(recipient, List.of(result));
+ results.put(recipient,
+ List.of(SendMessageResult.from(result,
+ account.getRecipientStore(),
+ account.getRecipientStore()::resolveRecipientAddress)));
} else if (recipient instanceof RecipientIdentifier.Group group) {
final var result = sendHelper.sendAsGroupMessage(messageBuilder, group.groupId());
- results.put(recipient, result);
+ results.put(recipient,
+ result.stream()
+ .map(sendMessageResult -> SendMessageResult.from(sendMessageResult,
+ account.getRecipientStore(),
+ account.getRecipientStore()::resolveRecipientAddress))
+ .collect(Collectors.toList()));
}
}
return new SendMessageResults(timestamp, results);
}
- private void sendTypingMessage(
+ private SendMessageResults sendTypingMessage(
SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients
- ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
final var timestamp = System.currentTimeMillis();
for (var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single) {
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent());
final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient);
- sendHelper.sendTypingMessage(message, recipientId);
+ final var result = sendHelper.sendTypingMessage(message, recipientId);
+ results.put(recipient,
+ List.of(SendMessageResult.from(result,
+ account.getRecipientStore(),
+ account.getRecipientStore()::resolveRecipientAddress)));
} else if (recipient instanceof RecipientIdentifier.Group) {
final var groupId = ((RecipientIdentifier.Group) recipient).groupId();
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize()));
- sendHelper.sendGroupTypingMessage(message, groupId);
+ final var result = sendHelper.sendGroupTypingMessage(message, groupId);
+ results.put(recipient,
+ result.stream()
+ .map(r -> SendMessageResult.from(r,
+ account.getRecipientStore(),
+ account.getRecipientStore()::resolveRecipientAddress))
+ .collect(Collectors.toList()));
}
}
+ return new SendMessageResults(timestamp, results);
}
@Override
- public void sendTypingMessage(
+ public SendMessageResults sendTypingMessage(
TypingAction action, Set<RecipientIdentifier> recipients
- ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
- sendTypingMessage(action.toSignalService(), recipients);
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ return sendTypingMessage(action.toSignalService(), recipients);
}
@Override
- public void sendReadReceipt(
+ public SendMessageResults sendReadReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
- ) throws IOException, UntrustedIdentityException {
+ ) throws IOException {
+ final var timestamp = System.currentTimeMillis();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
messageIds,
- System.currentTimeMillis());
+ timestamp);
- sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
+ final var result = sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
+ return new SendMessageResults(timestamp,
+ Map.of(sender,
+ List.of(SendMessageResult.from(result,
+ account.getRecipientStore(),
+ account.getRecipientStore()::resolveRecipientAddress))));
}
@Override
- public void sendViewedReceipt(
+ public SendMessageResults sendViewedReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
- ) throws IOException, UntrustedIdentityException {
+ ) throws IOException {
+ final var timestamp = System.currentTimeMillis();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds,
- System.currentTimeMillis());
+ timestamp);
- sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
+ final var result = sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender));
+ return new SendMessageResults(timestamp,
+ Map.of(sender,
+ List.of(SendMessageResult.from(result,
+ account.getRecipientStore(),
+ account.getRecipientStore()::resolveRecipientAddress))));
}
@Override
if (attachments != null) {
messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments));
}
+ if (message.mentions().size() > 0) {
+ messageBuilder.withMentions(resolveMentions(message.mentions()));
+ }
+ if (message.quote().isPresent()) {
+ final var quote = message.quote().get();
+ messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(),
+ resolveSignalServiceAddress(resolveRecipient(quote.author())),
+ quote.message(),
+ List.of(),
+ resolveMentions(quote.mentions())));
+ }
+ }
+
+ private ArrayList<SignalServiceDataMessage.Mention> resolveMentions(final List<Message.Mention> mentionList) throws IOException {
+ final var mentions = new ArrayList<SignalServiceDataMessage.Mention>();
+ for (final var m : mentionList) {
+ final var recipientId = resolveRecipient(m.recipient());
+ mentions.add(new SignalServiceDataMessage.Mention(resolveSignalServiceAddress(recipientId).getAci(),
+ m.start(),
+ m.length()));
+ }
+ return mentions;
}
@Override
}
}
+ @Override
+ public void deleteRecipient(final RecipientIdentifier.Single recipient) throws IOException {
+ account.removeRecipient(resolveRecipient(recipient));
+ }
+
+ @Override
+ public void deleteContact(final RecipientIdentifier.Single recipient) throws IOException {
+ account.getContactStore().deleteContact(resolveRecipient(recipient));
+ }
+
@Override
public void setContactName(
RecipientIdentifier.Single recipient, String name
return resolveRecipientTrusted(new SignalServiceAddress(uuid, number));
}
- private UUID getRegisteredUser(final String number) throws IOException {
- final Map<String, UUID> uuidMap;
+ private ACI getRegisteredUser(final String number) throws IOException {
+ final Map<String, ACI> aciMap;
try {
- uuidMap = getRegisteredUsers(Set.of(number));
+ aciMap = getRegisteredUsers(Set.of(number));
} catch (NumberFormatException e) {
throw new IOException(number, e);
}
- final var uuid = uuidMap.get(number);
+ final var uuid = aciMap.get(number);
if (uuid == null) {
throw new IOException(number, null);
}
return uuid;
}
- private Map<String, UUID> getRegisteredUsers(final Set<String> numbers) throws IOException {
- final Map<String, UUID> registeredUsers;
+ private Map<String, ACI> getRegisteredUsers(final Set<String> numbers) throws IOException {
+ final Map<String, ACI> registeredUsers;
try {
registeredUsers = dependencies.getAccountManager()
.getRegisteredUsers(ServiceConfig.getIasKeyStore(),
throw new IOException(e);
}
- // Store numbers as recipients so we have the number/uuid association
- registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number)));
+ // Store numbers as recipients, so we have the number/uuid association
+ registeredUsers.forEach((number, aci) -> resolveRecipientTrusted(new SignalServiceAddress(aci, number)));
return registeredUsers;
}
}
@Override
- public void addReceiveHandler(final ReceiveMessageHandler handler) {
+ public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
if (isReceivingSynchronous) {
throw new IllegalStateException("Already receiving message synchronously.");
}
synchronized (messageHandlers) {
- messageHandlers.add(handler);
-
- startReceiveThreadIfRequired();
+ if (isWeakListener) {
+ weakHandlers.add(handler);
+ } else {
+ messageHandlers.add(handler);
+ startReceiveThreadIfRequired();
+ }
}
}
return;
}
receiveThread = new Thread(() -> {
+ logger.debug("Starting receiving messages");
while (!Thread.interrupted()) {
try {
receiveMessagesInternal(1L, TimeUnit.HOURS, false, (envelope, e) -> {
synchronized (messageHandlers) {
- for (ReceiveMessageHandler h : messageHandlers) {
+ Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> {
try {
h.handleMessage(envelope, e);
} catch (Exception ex) {
logger.warn("Message handler failed, ignoring", ex);
}
- }
+ });
}
});
break;
logger.warn("Receiving messages failed, retrying", e);
}
}
+ logger.debug("Finished receiving messages");
hasCaughtUpWithOldMessages = false;
synchronized (messageHandlers) {
receiveThread = null;
// Check if in the meantime another handler has been registered
if (!messageHandlers.isEmpty()) {
+ logger.debug("Another handler has been registered, starting receive thread again");
startReceiveThreadIfRequired();
}
}
public void removeReceiveHandler(final ReceiveMessageHandler handler) {
final Thread thread;
synchronized (messageHandlers) {
- thread = receiveThread;
- receiveThread = null;
+ weakHandlers.remove(handler);
messageHandlers.remove(handler);
- if (!messageHandlers.isEmpty() || isReceivingSynchronous) {
+ if (!messageHandlers.isEmpty() || receiveThread == null || isReceivingSynchronous) {
return;
}
+ thread = receiveThread;
+ receiveThread = null;
}
stopReceiveThread(thread);
) throws IOException {
retryFailedReceivedMessages(handler);
- Set<HandleAction> queuedActions = new HashSet<>();
+ // Use a Map here because java Set doesn't have a get method ...
+ Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
final var signalWebSocket = dependencies.getSignalWebSocket();
signalWebSocket.connect();
while (!Thread.interrupted()) {
SignalServiceEnvelope envelope;
final CachedMessage[] cachedMessage = {null};
- account.setLastReceiveTimestamp(System.currentTimeMillis());
+ final var nowMillis = System.currentTimeMillis();
+ if (nowMillis - account.getLastReceiveTimestamp() > 60000) {
+ account.setLastReceiveTimestamp(nowMillis);
+ }
logger.debug("Checking for new message from server");
try {
var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> {
logger.debug("New message received from server");
} else {
logger.debug("Received indicator that server queue is empty");
- handleQueuedActions(queuedActions);
+ handleQueuedActions(queuedActions.keySet());
queuedActions.clear();
hasCaughtUpWithOldMessages = true;
}
final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler);
- queuedActions.addAll(result.first());
+ for (final var h : result.first()) {
+ final var existingAction = queuedActions.get(h);
+ if (existingAction == null) {
+ queuedActions.put(h, h);
+ } else {
+ existingAction.mergeOther(h);
+ }
+ }
final var exception = result.second();
if (hasCaughtUpWithOldMessages) {
- handleQueuedActions(queuedActions);
+ handleQueuedActions(queuedActions.keySet());
queuedActions.clear();
}
if (cachedMessage[0] != null) {
}
}
}
- handleQueuedActions(queuedActions);
+ handleQueuedActions(queuedActions.keySet());
queuedActions.clear();
+ dependencies.getSignalWebSocket().disconnect();
}
@Override
return contactHelper.isContactBlocked(recipientId);
}
- @Override
- public File getAttachmentFile(String attachmentId) {
- return attachmentHelper.getAttachmentFile(attachmentId);
- }
-
@Override
public void sendContacts() throws IOException {
syncHelper.sendContacts();
/**
* Trust this the identity with this fingerprint
*
- * @param recipient username of the identity
+ * @param recipient account of the identity
* @param fingerprint Fingerprint
*/
@Override
/**
* Trust this the identity with this safety number
*
- * @param recipient username of the identity
+ * @param recipient account of the identity
* @param safetyNumber Safety number
*/
@Override
/**
* Trust this the identity with this scannable safety number
*
- * @param recipient username of the identity
+ * @param recipient account of the identity
* @param safetyNumber Scannable safety number
*/
@Override
/**
* Trust all keys of this identity without verification
*
- * @param recipient username of the identity
+ * @param recipient account of the identity
*/
@Override
public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) {
return identityHelper.trustIdentityAllKeys(recipientId);
}
+ @Override
+ public void addClosedListener(final Runnable listener) {
+ synchronized (closedListeners) {
+ closedListeners.add(listener);
+ }
+ }
+
private void handleIdentityFailure(
- final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure
+ final RecipientId recipientId,
+ final org.whispersystems.signalservice.api.messages.SendMessageResult.IdentityFailure identityFailure
) {
this.identityHelper.handleIdentityFailure(recipientId, identityFailure);
}
// Address in recipient store doesn't have a uuid, this shouldn't happen
// Try to retrieve the uuid from the server
final var number = address.getNumber().get();
- final UUID uuid;
+ final ACI aci;
try {
- uuid = getRegisteredUser(number);
+ aci = getRegisteredUser(number);
} catch (IOException e) {
logger.warn("Failed to get uuid for e164 number: {}", number, e);
// Return SignalServiceAddress with unknown UUID
return address.toSignalServiceAddress();
}
- return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid));
+ return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(aci));
}
private Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws IOException {
}
private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws IOException {
- if (recipient instanceof RecipientIdentifier.Uuid) {
- return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid());
+ if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
+ return account.getRecipientStore().resolveRecipient(ACI.from(uuidRecipient.uuid()));
} else {
final var number = ((RecipientIdentifier.Number) recipient).number();
return account.getRecipientStore().resolveRecipient(number, () -> {
@Override
public void close() throws IOException {
- close(true);
- }
-
- private void close(boolean closeAccount) throws IOException {
Thread thread;
synchronized (messageHandlers) {
+ weakHandlers.clear();
messageHandlers.clear();
thread = receiveThread;
receiveThread = null;
dependencies.getSignalWebSocket().disconnect();
- if (closeAccount && account != null) {
+ synchronized (closedListeners) {
+ closedListeners.forEach(Runnable::run);
+ closedListeners.clear();
+ }
+
+ if (account != null) {
account.close();
}
account = null;