zeptoclaw 0.3.0

Ultra-lightweight personal AI assistant framework
Documentation
//! Channel trait and types for ZeptoClaw
//!
//! This module defines the `Channel` trait that all communication channels
//! (Telegram, Discord, Slack, etc.) must implement, along with supporting types.

use async_trait::async_trait;

use crate::bus::OutboundMessage;
use crate::error::Result;

/// The `Channel` trait defines the interface for all communication channels.
///
/// Channels are responsible for:
/// - Receiving messages from users and publishing them to the message bus
/// - Sending outbound messages from agents back to users
/// - Managing their connection lifecycle (start/stop)
/// - Enforcing access control via allowlists
///
/// # Example Implementation
///
/// ```ignore
/// use async_trait::async_trait;
/// use zeptoclaw::channels::{Channel, BaseChannelConfig};
/// use zeptoclaw::bus::OutboundMessage;
/// use zeptoclaw::error::Result;
///
/// struct MyChannel {
///     config: BaseChannelConfig,
///     running: bool,
/// }
///
/// #[async_trait]
/// impl Channel for MyChannel {
///     fn name(&self) -> &str {
///         &self.config.name
///     }
///
///     async fn start(&mut self) -> Result<()> {
///         self.running = true;
///         Ok(())
///     }
///
///     async fn stop(&mut self) -> Result<()> {
///         self.running = false;
///         Ok(())
///     }
///
///     async fn send(&self, msg: OutboundMessage) -> Result<()> {
///         println!("Sending: {}", msg.content);
///         Ok(())
///     }
///
///     fn is_running(&self) -> bool {
///         self.running
///     }
///
///     fn is_allowed(&self, user_id: &str) -> bool {
///         self.config.is_allowed(user_id)
///     }
/// }
/// ```
#[async_trait]
pub trait Channel: Send + Sync {
    /// Returns the unique name of this channel (e.g., "telegram", "discord").
    ///
    /// This name is used for routing messages and logging purposes.
    fn name(&self) -> &str;

    /// Starts the channel, establishing connections and beginning to listen
    /// for incoming messages.
    ///
    /// # Errors
    ///
    /// Returns an error if the channel fails to start (e.g., invalid token,
    /// network failure, etc.).
    async fn start(&mut self) -> Result<()>;

    /// Stops the channel, cleaning up resources and closing connections.
    ///
    /// # Errors
    ///
    /// Returns an error if the channel fails to stop cleanly.
    async fn stop(&mut self) -> Result<()>;

    /// Sends an outbound message through this channel.
    ///
    /// # Arguments
    ///
    /// * `msg` - The outbound message to send
    ///
    /// # Errors
    ///
    /// Returns an error if the message fails to send (e.g., network failure,
    /// invalid chat ID, rate limiting, etc.).
    async fn send(&self, msg: OutboundMessage) -> Result<()>;

    /// Returns whether the channel is currently running and accepting messages.
    fn is_running(&self) -> bool;

    /// Checks if a user is allowed to use this channel.
    ///
    /// # Arguments
    ///
    /// * `user_id` - The unique identifier of the user
    ///
    /// # Returns
    ///
    /// `true` if the user is allowed, `false` otherwise.
    fn is_allowed(&self, user_id: &str) -> bool;
}

/// Base configuration shared by all channels.
///
/// This struct provides common configuration options that most channels need,
/// including the channel name and an allowlist for access control.
///
/// # Example
///
/// ```
/// use zeptoclaw::channels::BaseChannelConfig;
///
/// let config = BaseChannelConfig {
///     name: "telegram".to_string(),
///     allowlist: vec!["user123".to_string(), "user456".to_string()],
/// };
///
/// assert!(config.is_allowed("user123"));
/// assert!(!config.is_allowed("user789"));
/// ```
#[derive(Debug, Clone, Default)]
pub struct BaseChannelConfig {
    /// The unique name of this channel
    pub name: String,
    /// List of allowed user IDs. If empty, all users are allowed.
    pub allowlist: Vec<String>,
}

impl BaseChannelConfig {
    /// Creates a new `BaseChannelConfig` with the given name and an empty allowlist.
    ///
    /// An empty allowlist means all users are allowed.
    ///
    /// # Arguments
    ///
    /// * `name` - The unique name for this channel
    ///
    /// # Example
    ///
    /// ```
    /// use zeptoclaw::channels::BaseChannelConfig;
    ///
    /// let config = BaseChannelConfig::new("telegram");
    /// assert!(config.is_allowed("anyone")); // Empty allowlist = allow all
    /// ```
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            allowlist: Vec::new(),
        }
    }

    /// Creates a new `BaseChannelConfig` with the given name and allowlist.
    ///
    /// # Arguments
    ///
    /// * `name` - The unique name for this channel
    /// * `allowlist` - List of allowed user IDs
    ///
    /// # Example
    ///
    /// ```
    /// use zeptoclaw::channels::BaseChannelConfig;
    ///
    /// let config = BaseChannelConfig::with_allowlist("telegram", vec!["user1".to_string()]);
    /// assert!(config.is_allowed("user1"));
    /// assert!(!config.is_allowed("user2"));
    /// ```
    pub fn with_allowlist(name: &str, allowlist: Vec<String>) -> Self {
        Self {
            name: name.to_string(),
            allowlist,
        }
    }

    /// Checks if a user is allowed based on the allowlist.
    ///
    /// If the allowlist is empty, all users are allowed.
    /// Otherwise, only users in the allowlist are allowed.
    ///
    /// # Arguments
    ///
    /// * `user_id` - The unique identifier of the user to check
    ///
    /// # Example
    ///
    /// ```
    /// use zeptoclaw::channels::BaseChannelConfig;
    ///
    /// // With allowlist
    /// let config = BaseChannelConfig {
    ///     name: "test".to_string(),
    ///     allowlist: vec!["user1".to_string()],
    /// };
    /// assert!(config.is_allowed("user1"));
    /// assert!(!config.is_allowed("user2"));
    ///
    /// // Empty allowlist = allow all
    /// let open_config = BaseChannelConfig::new("test");
    /// assert!(open_config.is_allowed("anyone"));
    /// ```
    pub fn is_allowed(&self, user_id: &str) -> bool {
        self.allowlist.is_empty() || self.allowlist.contains(&user_id.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_base_channel_config_new() {
        let config = BaseChannelConfig::new("telegram");
        assert_eq!(config.name, "telegram");
        assert!(config.allowlist.is_empty());
    }

    #[test]
    fn test_base_channel_config_with_allowlist() {
        let config = BaseChannelConfig::with_allowlist(
            "discord",
            vec!["user1".to_string(), "user2".to_string()],
        );
        assert_eq!(config.name, "discord");
        assert_eq!(config.allowlist.len(), 2);
    }

    #[test]
    fn test_base_channel_config() {
        let config = BaseChannelConfig {
            name: "test".to_string(),
            allowlist: vec!["user1".to_string()],
        };
        assert!(config.is_allowed("user1"));
        assert!(!config.is_allowed("user2"));
    }

    #[test]
    fn test_empty_allowlist() {
        let config = BaseChannelConfig {
            name: "test".to_string(),
            allowlist: vec![],
        };
        assert!(config.is_allowed("anyone")); // empty allowlist = allow all
    }

    #[test]
    fn test_allowlist_with_multiple_users() {
        let config = BaseChannelConfig {
            name: "test".to_string(),
            allowlist: vec![
                "user1".to_string(),
                "user2".to_string(),
                "user3".to_string(),
            ],
        };
        assert!(config.is_allowed("user1"));
        assert!(config.is_allowed("user2"));
        assert!(config.is_allowed("user3"));
        assert!(!config.is_allowed("user4"));
    }

    #[test]
    fn test_base_channel_config_default() {
        let config = BaseChannelConfig::default();
        assert!(config.name.is_empty());
        assert!(config.allowlist.is_empty());
        assert!(config.is_allowed("anyone"));
    }

    #[test]
    fn test_base_channel_config_clone() {
        let config = BaseChannelConfig {
            name: "test".to_string(),
            allowlist: vec!["user1".to_string()],
        };
        let cloned = config.clone();
        assert_eq!(cloned.name, "test");
        assert_eq!(cloned.allowlist, vec!["user1"]);
    }
}