From 59a0bd87cdc5fc62083aa34d5987803639837808 Mon Sep 17 00:00:00 2001 From: Kevin Kickartz-Grabowsky <45273132+KevinKickass@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:28:13 +0100 Subject: [PATCH] Add --voice-note flag for send command (#1973) Add support for marking audio attachments as voice notes when sending messages. Voice notes are displayed inline with a play button in Signal clients, rather than as file attachments. This addresses a longstanding TODO in AttachmentUtils.java and resolves the feature request in #1601. Changes: - Add 'voiceNote' field to Message record - Pass voiceNote flag through AttachmentHelper to AttachmentUtils - Add .withVoiceNote() to SignalServiceAttachmentStream builder - Add --voice-note CLI argument to SendCommand - Support voiceNote parameter in JSON-RPC mode Usage: signal-cli send -a audio.m4a --voice-note +1234567890 JSON-RPC: {"method":"send","params":{"attachment":"audio.m4a","voiceNote":true,...}} Closes #1601 --- .../org/asamk/signal/manager/api/Message.java | 1 + .../manager/helper/AttachmentHelper.java | 12 ++++-- .../signal/manager/internal/ManagerImpl.java | 2 +- .../signal/manager/util/AttachmentUtils.java | 41 +++++++++++++------ .../asamk/signal/commands/SendCommand.java | 5 +++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 3 ++ 6 files changed, 47 insertions(+), 17 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Message.java b/lib/src/main/java/org/asamk/signal/manager/api/Message.java index 3080b8fb..b9d60b00 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Message.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Message.java @@ -7,6 +7,7 @@ public record Message( String messageText, List attachments, boolean viewOnce, + boolean voiceNote, List mentions, Optional quote, Optional sticker, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index 08526c72..cc2ba7d0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -44,8 +44,8 @@ public class AttachmentHelper { return attachmentStore.retrieveAttachment(id); } - public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { - final var attachmentStreams = createAttachmentStreams(attachments); + public List uploadAttachments(final List attachments, boolean voiceNote) throws AttachmentInvalidException, IOException { + final var attachmentStreams = createAttachmentStreams(attachments, voiceNote); try { // Upload attachments here, so we only upload once even for multiple recipients @@ -61,14 +61,18 @@ public class AttachmentHelper { } } - private List createAttachmentStreams(List attachments) throws AttachmentInvalidException, IOException { + public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { + return uploadAttachments(attachments, false); + } + + private List createAttachmentStreams(List attachments, boolean voiceNote) throws AttachmentInvalidException, IOException { if (attachments == null) { return null; } final var signalServiceAttachments = new ArrayList(attachments.size()); for (var attachment : attachments) { final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec(); - signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, uploadSpec)); + signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, voiceNote, uploadSpec)); } return signalServiceAttachments; } 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 8e5ba747..7a6785e9 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 @@ -843,7 +843,7 @@ public class ManagerImpl implements Manager { messageBuilder.withBody(message.messageText()); } if (!message.attachments().isEmpty()) { - final var uploadedAttachments = context.getAttachmentHelper().uploadAttachments(message.attachments()); + final var uploadedAttachments = context.getAttachmentHelper().uploadAttachments(message.attachments(), message.voiceNote()); if (!additionalAttachments.isEmpty()) { additionalAttachments.addAll(uploadedAttachments); messageBuilder.withAttachments(additionalAttachments); diff --git a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java index bab4c433..5e2a3c6b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java @@ -14,32 +14,49 @@ public class AttachmentUtils { public static SignalServiceAttachmentStream createAttachmentStream( String attachment, + boolean voiceNote, ResumableUploadSpec resumableUploadSpec ) throws AttachmentInvalidException { try { final var streamDetails = Utils.createStreamDetails(attachment); - return createAttachmentStream(streamDetails.first(), streamDetails.second(), resumableUploadSpec); + return createAttachmentStream(streamDetails.first(), streamDetails.second(), voiceNote, resumableUploadSpec); } catch (IOException e) { throw new AttachmentInvalidException(attachment, e); } } + public static SignalServiceAttachmentStream createAttachmentStream( + String attachment, + ResumableUploadSpec resumableUploadSpec + ) throws AttachmentInvalidException { + return createAttachmentStream(attachment, false, resumableUploadSpec); + } + + public static SignalServiceAttachmentStream createAttachmentStream( + StreamDetails streamDetails, + Optional name, + boolean voiceNote, + ResumableUploadSpec resumableUploadSpec + ) throws ResumeLocationInvalidException { + final var uploadTimestamp = System.currentTimeMillis(); + return SignalServiceAttachmentStream.newStreamBuilder() + .withStream(streamDetails.getStream()) + .withContentType(streamDetails.getContentType()) + .withLength(streamDetails.getLength()) + .withFileName(name.orElse(null)) + .withVoiceNote(voiceNote) + .withUploadTimestamp(uploadTimestamp) + .withResumableUploadSpec(resumableUploadSpec) + .withUuid(UUID.randomUUID()) + .build(); + } + public static SignalServiceAttachmentStream createAttachmentStream( StreamDetails streamDetails, Optional name, ResumableUploadSpec resumableUploadSpec ) throws ResumeLocationInvalidException { - // TODO maybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option - final var uploadTimestamp = System.currentTimeMillis(); - return SignalServiceAttachmentStream.newStreamBuilder() - .withStream(streamDetails.getStream()) - .withContentType(streamDetails.getContentType()) - .withLength(streamDetails.getLength()) - .withFileName(name.orElse(null)) - .withUploadTimestamp(uploadTimestamp) - .withResumableUploadSpec(resumableUploadSpec) - .withUuid(UUID.randomUUID()) - .build(); + return createAttachmentStream(streamDetails, name, false, resumableUploadSpec); } } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 21e39d3f..aa4bb957 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -109,6 +109,9 @@ public class SendCommand implements JsonRpcLocalCommand { .action(Arguments.storeTrue()) .help("Send the message without the urgent flag, so no push notification is triggered for the recipient. " + "The message will still be delivered in real-time if the recipient's app is active."); + subparser.addArgument("--voice-note") + .action(Arguments.storeTrue()) + .help("Mark audio attachments as voice notes. Voice notes are displayed inline in Signal clients."); } @Override @@ -171,6 +174,7 @@ public class SendCommand implements JsonRpcLocalCommand { attachments = List.of(); } final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once")); + final var voiceNote = Boolean.TRUE.equals(ns.getBoolean("voice-note")); final var selfNumber = m.getSelfNumber(); @@ -247,6 +251,7 @@ public class SendCommand implements JsonRpcLocalCommand { final var message = new Message(messageText, attachments, viewOnce, + voiceNote, mentions, Optional.ofNullable(quote), Optional.ofNullable(sticker), diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index f60cc42b..2f4f881a 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -238,6 +238,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { final var message = new Message(messageText, attachments, false, + false, List.of(), Optional.empty(), Optional.empty(), @@ -404,6 +405,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { final var message = new Message(messageText, attachments, false, + false, List.of(), Optional.empty(), Optional.empty(), @@ -451,6 +453,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { final var message = new Message(messageText, attachments, false, + false, List.of(), Optional.empty(), Optional.empty(),