Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crates/lucarne-channel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ use futures::stream::BoxStream;

pub use types::{
Attachment, ChannelError, ChannelEvent, ChatId, CommandQuery, CommandQueryResult, FileUpload,
IncomingAttachment, IncomingMessage, MessageId, OutgoingButton, OutgoingMessage, Result,
WorkspaceHandle, WorkspaceId,
IncomingAttachment, IncomingMessage, MessageId, NotificationPolicy, OutgoingButton,
OutgoingMessage, Result, WorkspaceHandle, WorkspaceId,
};

/// Describes how rendered text was produced so a channel impl knows
Expand Down Expand Up @@ -134,6 +134,7 @@ pub trait Channel: Send + Sync {
attachment: Attachment,
) -> Result<MessageId> {
let mut upload = FileUpload::new(attachment.filename, attachment.bytes);
upload = upload.with_notification(attachment.notification);
if let Some(caption) = attachment.caption {
upload = upload.with_caption(caption);
}
Expand Down Expand Up @@ -237,6 +238,7 @@ mod attachment_tests {
bytes: vec![1, 2, 3],
caption: Some("Logo".into()),
reply_to: Some(MessageId::new("source")),
notification: NotificationPolicy::Silent,
};

let id = channel.send_attachment(&target, attachment).await.unwrap();
Expand All @@ -251,5 +253,6 @@ mod attachment_tests {
files[0].reply_to.as_ref().map(|id| id.as_str()),
Some("source")
);
assert_eq!(files[0].notification, NotificationPolicy::Silent);
}
}
14 changes: 9 additions & 5 deletions crates/lucarne-channel/src/robust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ async fn send_with_fallback_inner(
format: TextFormat::Plain,
buttons: msg.buttons.clone(),
reply_to: msg.reply_to.clone(),
silent: msg.silent,
notification: msg.notification,
};
match channel.send_all(target, plain).await {
Ok(ids) => {
Expand Down Expand Up @@ -212,7 +212,9 @@ async fn file_fallback(
let filename = fallback_filename(stem);
info!(filename = %filename, reason = %reason, "uploading fallback file");
let caption = format!("↳ inline send failed ({}); see attached.", reason);
let mut file = FileUpload::new(filename, msg.body.as_bytes().to_vec()).with_caption(caption);
let mut file = FileUpload::new(filename, msg.body.as_bytes().to_vec())
.with_caption(caption)
.with_notification(msg.notification);
if let Some(reply_to) = msg.reply_to.clone() {
file = file.reply_to(reply_to);
}
Expand Down Expand Up @@ -256,7 +258,7 @@ fn fallback_filename(stem: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{ChatId, WorkspaceId};
use crate::types::{ChatId, NotificationPolicy, WorkspaceId};
use async_trait::async_trait;
use futures::stream::BoxStream;
use std::sync::Mutex;
Expand Down Expand Up @@ -327,12 +329,14 @@ mod tests {
.lock()
.unwrap()
.push(Err(ChannelError::FormatRejected("bad md".into())));
let msg = OutgoingMessage::markdown("# hi");
let msg = OutgoingMessage::markdown("# hi").silent();
let id = send_with_fallback(&ch, &handle(), msg, "reply")
.await
.unwrap();
assert_eq!(id.as_str(), "file");
assert!(ch.last_file.lock().unwrap().is_some());
let file = ch.last_file.lock().unwrap();
let file = file.as_ref().expect("fallback file");
assert_eq!(file.notification, NotificationPolicy::Silent);
}

#[tokio::test]
Expand Down
51 changes: 45 additions & 6 deletions crates/lucarne-channel/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ pub struct OutgoingButton {
pub data: String,
}

/// Per-message delivery notification intent.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum NotificationPolicy {
/// Let the platform alert the user normally.
#[default]
Notify,
/// Deliver without a push/alert where the platform supports it.
Silent,
}

impl NotificationPolicy {
pub fn is_silent(self) -> bool {
matches!(self, Self::Silent)
}
}

/// A message the bot wants to send. `format` tells the channel how to
/// render `body` (plain vs markdown). `buttons` is optional inline
/// keyboard laid out row by row.
Expand All @@ -78,9 +94,8 @@ pub struct OutgoingMessage {
/// If set, the platform should send this message as a reply to
/// the given source message.
pub reply_to: Option<MessageId>,
/// If true the channel should deliver the message silently (no
/// push notification); used for status pings.
pub silent: bool,
/// Whether this delivery may alert the user.
pub notification: NotificationPolicy,
}

impl OutgoingMessage {
Expand All @@ -90,7 +105,7 @@ impl OutgoingMessage {
format: super::TextFormat::Plain,
buttons: Vec::new(),
reply_to: None,
silent: false,
notification: NotificationPolicy::Notify,
}
}
pub fn markdown(body: impl Into<String>) -> Self {
Expand All @@ -99,7 +114,7 @@ impl OutgoingMessage {
format: super::TextFormat::Markdown,
buttons: Vec::new(),
reply_to: None,
silent: false,
notification: NotificationPolicy::Notify,
}
}
pub fn with_buttons(mut self, rows: Vec<Vec<OutgoingButton>>) -> Self {
Expand All @@ -110,8 +125,12 @@ impl OutgoingMessage {
self.reply_to = Some(id);
self
}
pub fn with_notification(mut self, notification: NotificationPolicy) -> Self {
self.notification = notification;
self
}
pub fn silent(mut self) -> Self {
self.silent = true;
self.notification = NotificationPolicy::Silent;
self
}
}
Expand All @@ -125,6 +144,7 @@ pub struct FileUpload {
pub bytes: Vec<u8>,
pub caption: Option<String>,
pub reply_to: Option<MessageId>,
pub notification: NotificationPolicy,
}

impl FileUpload {
Expand All @@ -134,6 +154,7 @@ impl FileUpload {
bytes,
caption: None,
reply_to: None,
notification: NotificationPolicy::Notify,
}
}
pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
Expand All @@ -144,6 +165,14 @@ impl FileUpload {
self.reply_to = Some(id);
self
}
pub fn with_notification(mut self, notification: NotificationPolicy) -> Self {
self.notification = notification;
self
}
pub fn silent(mut self) -> Self {
self.notification = NotificationPolicy::Silent;
self
}
}

/// An agent-originated attachment ready for channel delivery.
Expand All @@ -154,6 +183,7 @@ pub struct Attachment {
pub bytes: Vec<u8>,
pub caption: Option<String>,
pub reply_to: Option<MessageId>,
pub notification: NotificationPolicy,
}

impl Attachment {
Expand All @@ -164,6 +194,7 @@ impl Attachment {
bytes,
caption: None,
reply_to: None,
notification: NotificationPolicy::Notify,
}
}
pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
Expand All @@ -174,6 +205,14 @@ impl Attachment {
self.reply_to = Some(id);
self
}
pub fn with_notification(mut self, notification: NotificationPolicy) -> Self {
self.notification = notification;
self
}
pub fn silent(mut self) -> Self {
self.notification = NotificationPolicy::Silent;
self
}
}

/// A user-originated attachment (file, image, voice note, …). Payload
Expand Down
20 changes: 17 additions & 3 deletions crates/lucarne-telegram/src/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5275,7 +5275,9 @@ fn history_upload_from_bytes(
caption: Option<String>,
reply_to: MessageId,
) -> FileUpload {
let mut upload = FileUpload::new(format!("history-image.{ext}"), bytes).reply_to(reply_to);
let mut upload = FileUpload::new(format!("history-image.{ext}"), bytes)
.reply_to(reply_to)
.silent();
if let Some(caption) = caption.filter(|caption| !caption.trim().is_empty()) {
upload = upload.with_caption(caption);
}
Expand Down Expand Up @@ -7222,7 +7224,10 @@ mod tests {
assert_eq!(sent[0].format, lucarne_channel::TextFormat::Markdown);
assert!(sent[0].body.contains("Lucarne update available"));
assert!(sent[0].reply_to.is_none());
assert!(!sent[0].silent);
assert_eq!(
sent[0].notification,
lucarne_channel::NotificationPolicy::Notify
);
drop(sent);

assert_eq!(
Expand Down Expand Up @@ -11444,6 +11449,15 @@ done
&notification_topic.chat,
&MessageId::new("sent-3"),
),
None,
"deleted silent preview should not route replies"
);
assert_eq!(
bot.state.resolve_message_session_binding(
channel.name(),
&notification_topic.chat,
&MessageId::new("sent-4"),
),
Some(provider_session_id),
"final assistant message must route replies"
);
Expand All @@ -11453,7 +11467,7 @@ done
message_id: MessageId::new("user-2"),
chat: notification_topic.chat.clone(),
workspace: Some(notification_topic.workspace.clone()),
reply_to: Some(MessageId::new("sent-3")),
reply_to: Some(MessageId::new("sent-4")),
user: "alice".into(),
text: Some("second follow-up".into()),
attachments: Vec::new(),
Expand Down
15 changes: 14 additions & 1 deletion crates/lucarne-telegram/src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ impl Channel for TelegramChannel {
if let Some(t) = thread {
req = req.message_thread_id(t);
}
if msg.silent {
if msg.notification.is_silent() {
req = req.disable_notification(true);
}
if idx == 0 {
Expand Down Expand Up @@ -491,6 +491,9 @@ impl Channel for TelegramChannel {
if let Some(cap) = file.caption {
req = req.caption(cap);
}
if file.notification.is_silent() {
req = req.disable_notification(true);
}
if let Some(reply_to) = file.reply_to.as_ref() {
req = req.reply_parameters(
ReplyParameters::new(parse_tg_message_id(reply_to)?).allow_sending_without_reply(),
Expand Down Expand Up @@ -528,6 +531,7 @@ impl Channel for TelegramChannel {
bytes,
caption,
reply_to,
notification,
} = attachment;
let input = InputFile::memory(bytes).file_name(filename);
let reply_parameters = reply_to
Expand All @@ -544,6 +548,9 @@ impl Channel for TelegramChannel {
if let Some(cap) = caption {
req = req.caption(cap);
}
if notification.is_silent() {
req = req.disable_notification(true);
}
if let Some(reply) = reply_parameters {
req = req.reply_parameters(reply);
}
Expand All @@ -561,6 +568,9 @@ impl Channel for TelegramChannel {
if let Some(cap) = caption {
req = req.caption(cap);
}
if notification.is_silent() {
req = req.disable_notification(true);
}
if let Some(reply) = reply_parameters {
req = req.reply_parameters(reply);
}
Expand All @@ -578,6 +588,9 @@ impl Channel for TelegramChannel {
if let Some(cap) = caption {
req = req.caption(cap);
}
if notification.is_silent() {
req = req.disable_notification(true);
}
if let Some(reply) = reply_parameters {
req = req.reply_parameters(reply);
}
Expand Down
Loading