diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index bdb3dbc97f..7d390bf067 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -368,4 +368,7 @@ interface ReceiveMessageHandler { void handleMessage(MessageEnvelope envelope, Throwable e); } + + SendMessageResults sendStoryMessage(Message message, RecipientIdentifier.Group recipient) + throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 2fb550032d..a42975897c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -31,6 +31,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; @@ -53,6 +55,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; import okio.ByteString; @@ -331,6 +334,66 @@ private List sendAsGroupMessage( editTargetTimestamp); } + public List sendGroupStoryMessage( + final SignalServiceStoryMessage message, + final GroupInfo g + ) throws IOException { + final var messageSender = dependencies.getMessageSender(); + final var messageSendLogStore = account.getMessageSendLogStore(); + final AtomicLong entryId = new AtomicLong(-1); + final boolean urgent = true; + final long timestamp = System.currentTimeMillis(); + // remove sender/self + final Set recipientIds = g.getMembersWithout(account.getSelfRecipientId()); + final List distributionListIds = List.of(g.getGroupId().toBase64()); + final Set messageRecipients = recipientIds.stream().map(i -> { + SignalServiceAddress ssa = context.getRecipientHelper().resolveSignalServiceAddress(i); + return new SignalServiceStoryMessageRecipient(ssa, distributionListIds, true); + }).collect(Collectors.toSet()); + final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupStory( + g.getDistributionId(), + Optional.of(g.getGroupId().serialize()), + recipients, + unidentifiedAccess, + groupSendEndorsements, + true, + message, + timestamp, + messageRecipients, + sendResult -> { + logger.trace("Partial message send results: {}", sendResult.size()); + synchronized (entryId) { + if (entryId.get() == -1) { + final var newId = messageSendLogStore.insertIfPossible(timestamp, + sendResult, + ContentHint.RESENDABLE, + urgent); + entryId.set(newId); + } else { + messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult); + } + } + synchronized (entryId) { + if (entryId.get() == -1) { + final var newId = messageSendLogStore.insertIfPossible(timestamp, + sendResult, + ContentHint.RESENDABLE, + urgent); + entryId.set(newId); + } else { + messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult); + } + } + }); + final var results = sendStoryMessageInternal(senderKeySender, recipientIds, g.getDistributionId()); + + for (var r : results) { + handleSendMessageResult(r); + } + + return results; + } + private List sendGroupMessage( final SignalServiceDataMessage message, final Set recipientIds, @@ -466,6 +529,44 @@ private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundExcept return g; } + private List sendStoryMessageInternal( + final SenderKeySenderHandler senderKeySender, + final Set recipientIds, + final DistributionId distributionId + ) throws IOException { + long startTime = System.currentTimeMillis(); + Set senderKeyTargets = distributionId == null + ? Set.of() + : getSenderKeyCapableRecipientIds(recipientIds); + final var allResults = new ArrayList(); + + if (!senderKeyTargets.isEmpty()) { + final var results = sendGroupMessageInternalWithSenderKey(senderKeySender, + senderKeyTargets, + distributionId, + false); + + if (results == null) { + senderKeyTargets = Set.of(); + } else { + results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add); + final var recipientResolver = account.getRecipientResolver(); + final var failedTargets = results.stream() + .filter(r -> !r.isSuccess()) + .map(r -> recipientResolver.resolveRecipient(r.getAddress())) + .toList(); + if (!failedTargets.isEmpty()) { + senderKeyTargets = new HashSet<>(senderKeyTargets); + failedTargets.forEach(senderKeyTargets::remove); + } + } + } + + final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime); + logger.debug("Sending took {}", duration.toString()); + return allResults; + } + private List sendGroupMessageInternal( final LegacySenderHandler legacySender, final SenderKeySenderHandler senderKeySender, @@ -546,12 +647,12 @@ private Set getSenderKeyCapableRecipientIds(final Set senderKeyTargets.add(recipientId); } - +/* if (senderKeyTargets.size() < 2) { logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size()); return Set.of(); } - +*/ logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size()); return senderKeyTargets; } diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index 403b9adf7d..e7874fad95 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -16,6 +16,34 @@ */ package org.asamk.signal.manager.internal; +import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES; +import static org.signal.core.util.StringExtensionsKt.splitByByteLength; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.AlreadyReceivingException; import org.asamk.signal.manager.api.AttachmentInvalidException; @@ -78,6 +106,7 @@ import org.asamk.signal.manager.storage.AvatarStore; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; @@ -97,6 +126,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServicePreview; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; @@ -109,41 +139,14 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.StreamDetails; +import org.whispersystems.signalservice.internal.push.BodyRange; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.schedulers.Schedulers; import okio.Utf8; -import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES; -import static org.signal.core.util.StringExtensionsKt.splitByByteLength; - public class ManagerImpl implements Manager { private static final Logger logger = LoggerFactory.getLogger(ManagerImpl.class); @@ -750,6 +753,35 @@ public SendMessageResults sendMessage( return sendMessage(messageBuilder, recipients, notifySelf); } + + @Override + public SendMessageResults sendStoryMessage(Message message, RecipientIdentifier.Group idGroup + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException { + final var selfProfile = context.getProfileHelper().getSelfProfile(); + if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) { + logger.warn( + "No profile name set. When sending a message it's recommended to set a profile name with the updateProfile command. This may become mandatory in the future."); + } + final var profileKey = account.getProfileKey().serialize(); + GroupInfoV2 groupInfo = (GroupInfoV2) context.getGroupHelper().getGroup(idGroup.groupId()); + List attachments = message.attachments(); + List bodyRanges = message.textStyles().stream().map(t -> t.toBodyRange()).toList(); + + SignalServiceStoryMessage storyMessage = null; + if (attachments != null && attachments.size() > 0) { + var attachment = context.getAttachmentHelper().uploadAttachment(attachments.get(0)); + storyMessage = SignalServiceStoryMessage.forFileAttachment(profileKey, null, attachment, true, bodyRanges); + } else { + //SignalServiceTextAttachment textBuilder = new SignalServiceTextAttachment. + //storyMessage = SignalServiceStoryMessage.forTextAttachment(profileKey, ssgroup, textBuilder.build(), true, bodyRanges); + } + var results = new HashMap>(); + long timestamp = System.currentTimeMillis(); + final var result = context.getSendHelper().sendGroupStoryMessage(storyMessage, groupInfo); + results.put(idGroup, result.stream().map(this::toSendMessageResult).toList()); + return new SendMessageResults(timestamp, results); + } + @Override public SendMessageResults sendEditMessage( Message message, diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index ac1f6d509b..b896f90b0c 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -754,4 +754,6 @@ public UnregisteredRecipient(final String message) { } } } + + long sendStoryMessage(String messageText, List attachments, byte[] groupId); } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 9784171aef..13ffcead1e 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -124,6 +124,13 @@ public void handleCommand( groupIdStrings, usernameStrings); + boolean isStory = false; + + if (recipientIdentifiers.size() > 0 + && recipientIdentifiers.iterator().next() instanceof RecipientIdentifier.Group) { + isStory = Boolean.TRUE.equals(ns.getBoolean("story")); + } + final var isEndSession = Boolean.TRUE.equals(ns.getBoolean("end-session")); if (isEndSession) { final var singleRecipients = recipientIdentifiers.stream() @@ -244,7 +251,8 @@ public void handleCommand( textStyles); var results = editTimestamp != null ? m.sendEditMessage(message, recipientIdentifiers, editTimestamp) - : m.sendMessage(message, recipientIdentifiers, notifySelf); + : isStory ? m.sendStoryMessage(message, (RecipientIdentifier.Group) recipientIdentifiers.iterator().next()) + : m.sendMessage(message, recipientIdentifiers, notifySelf); outputResult(outputWriter, results); } catch (AttachmentInvalidException | IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 5658d0d326..9c24f8025f 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -427,6 +427,16 @@ public SendMessageResults sendMessage( groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId)); } + @Override + public SendMessageResults sendStoryMessage( + final Message message, final RecipientIdentifier.Group recipient + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return handleMessage(Set.of(recipient), + numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers), + () -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()), + groupId -> signal.sendStoryMessage(message.messageText(), message.attachments(), groupId)); + } + @Override public SendMessageResults sendEditMessage( final Message message, @@ -1150,4 +1160,5 @@ public InputStream retrieveSticker(final StickerPackId stickerPackId, final int private T getValue(final Map> stringVariantMap, final String field) { return (T) stringVariantMap.get(field).getValue(); } + } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ac77df99d2..0b289f019a 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -464,6 +464,31 @@ public long sendGroupMessage(final String messageText, final List attach } } + @Override + public long sendStoryMessage(final String messageText, final List attachments, final byte[] groupId) { + try { + final var message = new Message(messageText, + attachments, + List.of(), + Optional.empty(), + Optional.empty(), + List.of(), + Optional.empty(), + List.of()); + var results = m.sendStoryMessage(message,getGroupRecipientIdentifier(groupId)); + checkSendMessageResults(results); + return results.timestamp(); + } catch (IOException | InvalidStickerException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } catch (UnregisteredRecipientException e) { + throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered."); + } + } + @Override public void sendGroupTyping( final byte[] groupId,