Skip to content
Draft
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
59 changes: 59 additions & 0 deletions crates/channels/src/activity_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! User-facing channel Activity log visibility.

/// User-facing activity log visibility for channel replies.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActivityLogMode {
/// Show all buffered activity entries.
#[default]
All,
/// Show failed tool/activity entries only.
ErrorsOnly,
/// Do not append or send activity log entries.
Off,
}

impl ActivityLogMode {
pub fn includes(self, kind: ChannelStatusLogKind) -> bool {
match self {
Self::All => true,
Self::ErrorsOnly => kind == ChannelStatusLogKind::Error,
Self::Off => false,
}
}

pub fn is_all(&self) -> bool {
*self == Self::All
}
}

/// Type of buffered channel activity entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChannelStatusLogKind {
Info,
Error,
}

/// Buffered activity entry appended to channel replies when enabled.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ChannelStatusLogEntry {
pub kind: ChannelStatusLogKind,
pub message: String,
}

impl ChannelStatusLogEntry {
pub fn info(message: impl Into<String>) -> Self {
Self {
kind: ChannelStatusLogKind::Info,
message: message.into(),
}
}

pub fn error(message: impl Into<String>) -> Self {
Self {
kind: ChannelStatusLogKind::Error,
message: message.into(),
}
}
}
20 changes: 19 additions & 1 deletion crates/channels/src/config_view.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::gating::{DmPolicy, GroupPolicy};
use crate::{
activity_log::ActivityLogMode,
gating::{DmPolicy, GroupPolicy},
};

/// Typed read-only view of common channel account config fields.
///
Expand Down Expand Up @@ -27,6 +30,11 @@ pub trait ChannelConfigView: Send + Sync + std::fmt::Debug {
/// Provider name associated with the model.
fn model_provider(&self) -> Option<&str>;

/// Default activity log visibility for this channel account.
fn activity_log(&self) -> ActivityLogMode {
ActivityLogMode::All
}

/// Default agent id for this channel account.
fn agent_id(&self) -> Option<&str> {
None
Expand Down Expand Up @@ -59,6 +67,16 @@ pub trait ChannelConfigView: Send + Sync + std::fmt::Debug {
None
}

/// Activity log override for a specific channel/chat ID.
fn channel_activity_log(&self, _channel_id: &str) -> Option<ActivityLogMode> {
None
}

/// Activity log override for a specific user.
fn user_activity_log(&self, _user_id: &str) -> Option<ActivityLogMode> {
None
}

/// Agent override for a specific user.
fn user_agent_id(&self, _user_id: &str) -> Option<&str> {
None
Expand Down
2 changes: 2 additions & 0 deletions crates/channels/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! ChannelPlugin trait with sub-traits for config, auth, inbound/outbound
//! messaging, status, and gateway lifecycle.

pub mod activity_log;
pub mod channel_webhook_middleware;
pub mod commands;
pub mod config_view;
Expand All @@ -18,6 +19,7 @@ pub mod registry;
pub mod store;

pub use {
activity_log::{ActivityLogMode, ChannelStatusLogEntry, ChannelStatusLogKind},
channel_webhook_middleware::{
ChannelWebhookDedupeResult, ChannelWebhookRatePolicy, ChannelWebhookRejection,
ChannelWebhookVerifier, TimestampGuard, VerifiedChannelWebhook,
Expand Down
33 changes: 32 additions & 1 deletion crates/channels/src/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::sync::Arc;

use {
crate::activity_log::ActivityLogMode,
async_trait::async_trait,
moltis_common::{hooks::ChannelBinding, types::ReplyPayload},
tokio::sync::mpsc,
Expand Down Expand Up @@ -645,6 +646,16 @@ pub struct ChannelReplyTarget {
/// top-level chat.
#[serde(default, skip_serializing_if = "Option::is_none")]
Comment thread
s-salamatov marked this conversation as resolved.
pub thread_id: Option<String>,
/// User/sender ID associated with this target, when known.
///
/// This is persisted with channel bindings so later web-originated replies
/// can re-evaluate per-user channel settings without storing stale effective
/// runtime decisions.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sender_id: Option<String>,
Comment thread
s-salamatov marked this conversation as resolved.
Comment thread
s-salamatov marked this conversation as resolved.
/// Effective activity log visibility for this reply target.
#[serde(default, skip_serializing_if = "ActivityLogMode::is_all")]
pub activity_log: ActivityLogMode,
}

impl ChannelReplyTarget {
Expand All @@ -659,6 +670,16 @@ impl ChannelReplyTarget {
None => std::borrow::Cow::Borrowed(&self.chat_id),
}
}

/// Returns the route data safe to persist in session metadata.
///
/// Effective settings such as `activity_log` are intentionally reset so
/// active channel-bound sessions pick up later config changes.
pub fn for_persistence(&self) -> Self {
let mut target = self.clone();
target.activity_log = ActivityLogMode::All;
target
}
}

impl From<&ChannelReplyTarget> for ChannelBinding {
Expand All @@ -671,7 +692,7 @@ impl From<&ChannelReplyTarget> for ChannelBinding {
account_id: Some(target.account_id.clone()),
chat_id: Some(target.chat_id.clone()),
chat_type: target.channel_type.classify_chat(&target.chat_id),
sender_id: None,
sender_id: target.sender_id.clone(),
}
}
}
Expand Down Expand Up @@ -1101,6 +1122,8 @@ mod tests {
chat_id: "42".into(),
message_id: None,
thread_id: None,
sender_id: None,
activity_log: ActivityLogMode::All,
};
assert!(!sink.update_location(&target, 48.8566, 2.3522).await);
}
Expand All @@ -1113,6 +1136,8 @@ mod tests {
chat_id: "12345".into(),
message_id: None,
thread_id: None,
sender_id: None,
activity_log: ActivityLogMode::All,
};
assert_eq!(target.outbound_to().as_ref(), "12345");
}
Expand All @@ -1125,6 +1150,8 @@ mod tests {
chat_id: "-100999".into(),
message_id: None,
thread_id: Some("42".into()),
sender_id: None,
activity_log: ActivityLogMode::All,
};
assert_eq!(target.outbound_to().as_ref(), "-100999:42");
}
Expand All @@ -1137,6 +1164,8 @@ mod tests {
chat_id: "-100999".into(),
message_id: None,
thread_id: Some("42".into()),
sender_id: None,
activity_log: ActivityLogMode::All,
};
let json = serde_json::to_string(&target).unwrap();
assert!(json.contains("\"thread_id\":\"42\""));
Expand Down Expand Up @@ -1438,6 +1467,8 @@ mod tests {
chat_id: "-100999".into(),
message_id: Some("7".into()),
thread_id: Some("42".into()),
sender_id: None,
activity_log: ActivityLogMode::All,
};

let binding: ChannelBinding = (&target).into();
Expand Down
35 changes: 35 additions & 0 deletions crates/channels/src/plugin/tests/binding_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ fn resolve_session_channel_binding_extracts_channel_target() {
chat_id: "-100123".into(),
message_id: Some("11".into()),
thread_id: None,
sender_id: Some("user-123".into()),
activity_log: ActivityLogMode::All,
})
.unwrap_or_else(|error| panic!("serialize binding: {error}"));

Expand All @@ -38,6 +40,7 @@ fn resolve_session_channel_binding_extracts_channel_target() {
assert_eq!(binding.account_id.as_deref(), Some("bot-main"));
assert_eq!(binding.chat_id.as_deref(), Some("-100123"));
assert_eq!(binding.chat_type.as_deref(), Some("channel_or_supergroup"));
assert_eq!(binding.sender_id.as_deref(), Some("user-123"));
}

#[test]
Expand All @@ -47,3 +50,35 @@ fn resolve_session_channel_binding_returns_error_for_invalid_json() {
.unwrap_or_else(|| panic!("invalid binding json should fail"));
assert!(error.is_syntax());
}

#[test]
fn channel_reply_target_defaults_activity_log_to_all() {
let target: ChannelReplyTarget = serde_json::from_value(serde_json::json!({
"channel_type": "telegram",
"account_id": "bot1",
"chat_id": "123",
"message_id": "42"
}))
.unwrap();

assert_eq!(target.activity_log, ActivityLogMode::All);
let serialized = serde_json::to_value(&target).unwrap();
assert!(serialized.get("activity_log").is_none());
}

#[test]
fn reply_target_persistence_keeps_sender_but_not_activity_snapshot() {
let target = ChannelReplyTarget {
channel_type: ChannelType::Telegram,
account_id: "bot1".into(),
chat_id: "-100999".into(),
message_id: Some("7".into()),
thread_id: None,
sender_id: Some("123".into()),
activity_log: ActivityLogMode::Off,
};

let persisted = serde_json::to_value(target.for_persistence()).unwrap();
assert_eq!(persisted["sender_id"], "123");
assert!(persisted.get("activity_log").is_none());
}
Loading