*/
package org.asamk.signal.manager;
+import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.InvalidStickerException;
+import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.Message;
+import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException;
import org.asamk.signal.manager.api.StickerPackId;
import org.asamk.signal.manager.api.StickerPackInvalidException;
import org.asamk.signal.manager.api.StickerPackUrl;
+import org.asamk.signal.manager.api.TextStyle;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.MimeUtils;
import org.asamk.signal.manager.util.StickerUtils;
+import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
context.getSyncHelper().sendSyncFetchProfileMessage();
}
+ void refreshCurrentUsername() throws IOException, BaseUsernameException {
+ context.getAccountHelper().refreshCurrentUsername();
+ }
+
+ @Override
+ public String setUsername(final String username) throws IOException, InvalidUsernameException {
+ try {
+ return context.getAccountHelper().reserveUsername(username);
+ } catch (BaseUsernameException e) {
+ throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
+ }
+ }
+
+ @Override
+ public void deleteUsername() throws IOException {
+ context.getAccountHelper().deleteUsername();
+ }
+
@Override
public void unregister() throws IOException {
context.getAccountHelper().unregister();
@Override
public Pair<GroupId, SendGroupMessageResults> createGroup(
- String name, Set<RecipientIdentifier.Single> members, File avatarFile
+ String name, Set<RecipientIdentifier.Single> members, String avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException {
return context.getGroupHelper()
.createGroup(name,
private SendMessageResults sendMessage(
SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
+ return sendMessage(messageBuilder, recipients, Optional.empty());
+ }
+
+ private SendMessageResults sendMessage(
+ SignalServiceDataMessage.Builder messageBuilder,
+ Set<RecipientIdentifier> recipients,
+ Optional<Long> editTargetTimestamp
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = System.currentTimeMillis();
if (recipient instanceof RecipientIdentifier.Single single) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(single);
- final var result = context.getSendHelper().sendMessage(messageBuilder, recipientId);
+ final var result = context.getSendHelper()
+ .sendMessage(messageBuilder, recipientId, editTargetTimestamp);
results.put(recipient, List.of(toSendMessageResult(result)));
} catch (UnregisteredRecipientException e) {
results.put(recipient,
List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
}
} else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
- final var result = context.getSendHelper().sendSelfMessage(messageBuilder);
+ final var result = context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
results.put(recipient, List.of(toSendMessageResult(result)));
} else if (recipient instanceof RecipientIdentifier.Group group) {
- final var result = context.getSendHelper().sendAsGroupMessage(messageBuilder, group.groupId());
+ final var result = context.getSendHelper()
+ .sendAsGroupMessage(messageBuilder, group.groupId(), editTargetTimestamp);
results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
}
}
return sendMessage(messageBuilder, recipients);
}
+ @Override
+ public SendMessageResults sendEditMessage(
+ Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
+ ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
+ final var messageBuilder = SignalServiceDataMessage.newBuilder();
+ applyMessage(messageBuilder, message);
+ return sendMessage(messageBuilder, recipients, Optional.of(editTargetTimestamp));
+ }
+
private void applyMessage(
final SignalServiceDataMessage.Builder messageBuilder, final Message message
) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
final var textAttachment = AttachmentUtils.createAttachmentStream(new StreamDetails(new ByteArrayInputStream(
messageBytes), MimeUtils.LONG_TEXT, messageBytes.length), Optional.empty());
messageBuilder.withBody(message.messageText().substring(0, 2000));
- messageBuilder.withAttachment(textAttachment);
+ messageBuilder.withAttachment(context.getAttachmentHelper().uploadAttachment(textAttachment));
} else {
messageBuilder.withBody(message.messageText());
}
if (message.mentions().size() > 0) {
messageBuilder.withMentions(resolveMentions(message.mentions()));
}
+ if (message.textStyles().size() > 0) {
+ messageBuilder.withBodyRanges(message.textStyles().stream().map(TextStyle::toBodyRange).toList());
+ }
if (message.quote().isPresent()) {
final var quote = message.quote().get();
messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(),
quote.message(),
List.of(),
resolveMentions(quote.mentions()),
- SignalServiceDataMessage.Quote.Type.NORMAL));
+ SignalServiceDataMessage.Quote.Type.NORMAL,
+ quote.textStyles().stream().map(TextStyle::toBodyRange).toList()));
}
if (message.sticker().isPresent()) {
final var sticker = message.sticker().get();
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
for (final var recipient : recipients) {
- if (recipient instanceof RecipientIdentifier.Single r) {
+ if (recipient instanceof RecipientIdentifier.Uuid u) {
+ account.getMessageSendLogStore()
+ .deleteEntryForRecipientNonGroup(targetSentTimestamp, ServiceId.from(u.uuid()));
+ } else if (recipient instanceof RecipientIdentifier.Single r) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
- account.getMessageSendLogStore()
- .deleteEntryForRecipientNonGroup(targetSentTimestamp,
- account.getRecipientAddressResolver()
- .resolveRecipientAddress(recipientId)
- .getServiceId());
+ final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
+ if (address.serviceId().isPresent()) {
+ account.getMessageSendLogStore()
+ .deleteEntryForRecipientNonGroup(targetSentTimestamp, address.serviceId().get());
+ }
} catch (UnregisteredRecipientException ignored) {
}
} else if (recipient instanceof RecipientIdentifier.Group r) {
byte[] receipt, String note, RecipientIdentifier.Single recipient
) throws IOException {
final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note);
- final var payment = new SignalServiceDataMessage.Payment(paymentNotification);
+ final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null);
final var messageBuilder = SignalServiceDataMessage.newBuilder().withPayment(payment);
try {
return sendMessage(messageBuilder, Set.of(recipient));
final var serviceId = context.getAccount()
.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId)
- .getServiceId();
- account.getAciSessionStore().deleteAllSessions(serviceId);
+ .serviceId();
+ if (serviceId.isPresent()) {
+ account.getAciSessionStore().deleteAllSessions(serviceId.get());
+ }
}
}
}
@Override
public void deleteRecipient(final RecipientIdentifier.Single recipient) {
- account.removeRecipient(account.getRecipientResolver().resolveRecipient(recipient.getIdentifier()));
+ final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
+ if (recipientIdOptional.isPresent()) {
+ account.removeRecipient(recipientIdOptional.get());
+ }
}
@Override
public void deleteContact(final RecipientIdentifier.Single recipient) {
- account.getContactStore()
- .deleteContact(account.getRecipientResolver().resolveRecipient(recipient.getIdentifier()));
+ final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
+ if (recipientIdOptional.isPresent()) {
+ account.getContactStore().deleteContact(recipientIdOptional.get());
+ }
}
@Override
@Override
public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
- if (isReceivingSynchronous) {
- throw new IllegalStateException("Already receiving message synchronously.");
- }
synchronized (messageHandlers) {
if (isWeakListener) {
weakHandlers.add(handler);
private static final AtomicInteger threadNumber = new AtomicInteger(0);
private void startReceiveThreadIfRequired() {
- if (receiveThread != null) {
+ if (receiveThread != null || isReceivingSynchronous) {
return;
}
receiveThread = new Thread(() -> {
logger.debug("Starting receiving messages");
- context.getReceiveHelper().receiveMessagesContinuously((envelope, e) -> {
- synchronized (messageHandlers) {
- final var handlers = Stream.concat(messageHandlers.stream(), weakHandlers.stream()).toList();
- handlers.forEach(h -> {
- try {
- h.handleMessage(envelope, e);
- } catch (Throwable ex) {
- logger.warn("Message handler failed, ignoring", ex);
- }
- });
- }
- });
+ context.getReceiveHelper().receiveMessagesContinuously(this::passReceivedMessageToHandlers);
logger.debug("Finished receiving messages");
synchronized (messageHandlers) {
receiveThread = null;
receiveThread.start();
}
+ private void passReceivedMessageToHandlers(MessageEnvelope envelope, Throwable e) {
+ synchronized (messageHandlers) {
+ Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> {
+ try {
+ h.handleMessage(envelope, e);
+ } catch (Throwable ex) {
+ logger.warn("Message handler failed, ignoring", ex);
+ }
+ });
+ }
+ }
+
@Override
public void removeReceiveHandler(final ReceiveMessageHandler handler) {
final Thread thread;
}
@Override
- public void receiveMessages(Duration timeout, ReceiveMessageHandler handler) throws IOException {
- receiveMessages(timeout, true, handler);
- }
-
- @Override
- public void receiveMessages(ReceiveMessageHandler handler) throws IOException {
- receiveMessages(Duration.ofMinutes(1), false, handler);
+ public void receiveMessages(
+ Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
+ ) throws IOException, AlreadyReceivingException {
+ receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler);
}
private void receiveMessages(
- Duration timeout, boolean returnOnTimeout, ReceiveMessageHandler handler
- ) throws IOException {
- if (isReceiving()) {
- throw new IllegalStateException("Already receiving message.");
+ Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler
+ ) throws IOException, AlreadyReceivingException {
+ synchronized (messageHandlers) {
+ if (isReceiving()) {
+ throw new AlreadyReceivingException("Already receiving message.");
+ }
+ isReceivingSynchronous = true;
+ receiveThread = Thread.currentThread();
}
- isReceivingSynchronous = true;
- receiveThread = Thread.currentThread();
try {
- context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, handler);
+ context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, maxMessages, (envelope, e) -> {
+ passReceivedMessageToHandlers(envelope, e);
+ handler.handleMessage(envelope, e);
+ });
} finally {
- receiveThread = null;
- isReceivingSynchronous = false;
+ synchronized (messageHandlers) {
+ receiveThread = null;
+ isReceivingSynchronous = false;
+ if (messageHandlers.size() > 0) {
+ startReceiveThreadIfRequired();
+ }
+ }
}
}
public List<Identity> getIdentities(RecipientIdentifier.Single recipient) {
ServiceId serviceId;
try {
- serviceId = account.getRecipientAddressResolver()
- .resolveRecipientAddress(context.getRecipientHelper().resolveRecipient(recipient))
- .getServiceId();
+ final var address = account.getRecipientAddressResolver()
+ .resolveRecipientAddress(context.getRecipientHelper().resolveRecipient(recipient));
+ if (address.serviceId().isEmpty()) {
+ return List.of();
+ }
+ serviceId = address.serviceId().get();
} catch (UnregisteredRecipientException e) {
return List.of();
}
}
}
+ @Override
+ public InputStream retrieveAttachment(final String id) throws IOException {
+ return context.getAttachmentHelper().retrieveAttachment(id).getStream();
+ }
+
@Override
public void close() {
Thread thread;