From 862567ac93196ab4a7c39f44b7183601751d59ff Mon Sep 17 00:00:00 2001 From: Tony Cebzanov Date: Thu, 9 Jul 2026 01:48:24 -0400 Subject: [PATCH] Add sendStory method for posting file attachment stories to My Story Adds Manager.sendStory(attachment, allowsReplies), which uploads a file attachment, builds a SignalServiceStoryMessage, and sends it to all registered, non-blocked, non-hidden contacts that haven't opted out of seeing the user's story (Contact.hideStory), excluding self. SendHelper gains sendStoryMessage(), which resolves recipient addresses and unidentified access and delegates to SignalServiceMessageSender.sendGroupStory() against DistributionId.MY_STORY, following the same address/access resolution pattern used for group sends. A sync transcript is sent afterwards via sendStorySyncMessage() so linked devices see the story was posted. This is core library plumbing only; no CLI command, stub implementations, or documentation are added yet. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01MR2KF56Qcf9qNH1URj3XWs --- .../org/asamk/signal/manager/Manager.java | 8 +++ .../signal/manager/helper/SendHelper.java | 49 +++++++++++++++++++ .../signal/manager/internal/ManagerImpl.java | 49 +++++++++++++++++++ 3 files changed, 106 insertions(+) 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 dac629e7..4c625e79 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -213,6 +213,14 @@ public interface Manager extends Closeable { long editTargetTimestamp ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; + /** + * Post a file attachment story to "My Story". + * + * @param attachment path to the file to upload and post as a story + * @param allowsReplies whether other users are allowed to reply to this story + */ + SendMessageResults sendStory(String attachment, boolean allowsReplies) throws IOException, AttachmentInvalidException; + SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients 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 87e67b7a..261ce228 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 @@ -35,6 +35,8 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult; 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.calls.SignalServiceCallMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; @@ -330,6 +332,53 @@ public class SendHelper { return result; } + /** + * Send a story message (file attachment) to "My Story". + */ + public List sendStoryMessage( + SignalServiceStoryMessage storyMessage, + long timestamp, + Set recipientIds, + boolean allowsReplies + ) throws IOException { + final var messageSender = dependencies.getMessageSender(); + + final var recipientIdList = List.copyOf(recipientIds); + final var addressesMap = recipientIdList.stream() + .collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress)); + final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds); + + final var addresses = recipientIdList.stream().map(addressesMap::get).toList(); + final var unidentifiedAccesses = recipientIdList.stream().map(unidentifiedAccessesMap::get).toList(); + final var storyMessageRecipients = recipientIdList.stream() + .map(id -> new SignalServiceStoryMessageRecipient(addressesMap.get(id), + List.of(DistributionId.MY_STORY.asUuid().toString()), + allowsReplies)) + .collect(Collectors.toSet()); + + final List results; + try { + results = messageSender.sendGroupStory(DistributionId.MY_STORY, + Optional.empty(), + addresses, + unidentifiedAccesses, + null, + false, + storyMessage, + timestamp, + storyMessageRecipients, + null); + } catch (UntrustedIdentityException | InvalidKeyException | NoSessionException | InvalidRegistrationIdException e) { + throw new IOException(e); + } + + for (var r : results) { + handleSendMessageResult(r); + } + + return results; + } + private List sendAsGroupMessage( final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g, 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 18c261b5..d5a72a88 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 @@ -104,10 +104,13 @@ import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; 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.SignalServiceStoryMessageRecipient; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; import org.whispersystems.signalservice.api.messages.calls.BusyMessage; @@ -116,6 +119,7 @@ import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; @@ -826,6 +830,51 @@ public class ManagerImpl implements Manager { return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp), message.urgent()); } + @Override + public SendMessageResults sendStory( + String attachment, + boolean allowsReplies + ) throws IOException, AttachmentInvalidException { + final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment); + final var storyMessage = SignalServiceStoryMessage.forFileAttachment(account.getProfileKey().serialize(), + null, + uploadedAttachment, + allowsReplies, + List.of()); + final var timestamp = getNextMessageTimestamp(); + + final var recipients = account.getRecipientStore() + .getRecipients(true, Optional.of(false), Set.of(), Optional.empty()); + final var recipientIds = recipients.stream() + .filter(r -> !r.getRecipientId().equals(account.getSelfRecipientId())) + .filter(r -> r.getContact() == null || !r.getContact().hideStory()) + .map(r -> r.getRecipientId()) + .collect(Collectors.toSet()); + + final var sendResults = context.getSendHelper() + .sendStoryMessage(storyMessage, timestamp, recipientIds, allowsReplies); + + final var storyMessageRecipients = sendResults.stream() + .filter(org.whispersystems.signalservice.api.messages.SendMessageResult::isSuccess) + .map(r -> new SignalServiceStoryMessageRecipient(r.getAddress(), + List.of(DistributionId.MY_STORY.asUuid().toString()), + allowsReplies)) + .collect(Collectors.toSet()); + try { + dependencies.getMessageSender().sendStorySyncMessage(storyMessage, timestamp, false, storyMessageRecipients); + } catch (UntrustedIdentityException e) { + throw new IOException(e); + } + + final var results = new HashMap>(); + for (final var sendResult : sendResults) { + final var result = toSendMessageResult(sendResult); + results.put(RecipientIdentifier.Single.fromAddress(result.address()), List.of(result)); + } + + return new SendMessageResults(timestamp, results); + } + private void applyMessage( final SignalServiceDataMessage.Builder messageBuilder, final Message message