bhcli

A TUI for chatting on LE PHP Chats
git clone https://git.dasho.dev/bhcli.git
Log | Files | Refs | README

bot_system.rs (54576B)


      1 use crate::ai_service::AIService;
      2 use crate::{PostType, Users};
      3 use anyhow::{anyhow, Result};
      4 use chrono::{DateTime, Datelike, Timelike, Utc};
      5 use crossbeam_channel::Sender;
      6 use log::{error, info, warn};
      7 use serde::{Deserialize, Serialize};
      8 use std::collections::HashMap;
      9 use std::fs::File;
     10 use std::io::Write;
     11 use std::path::{Path, PathBuf};
     12 use std::sync::{Arc, Mutex};
     13 use std::thread;
     14 use std::time::{Duration, SystemTime};
     15 use tokio::runtime::Runtime;
     16 
     17 /// Represents a chat message stored by the bot
     18 #[derive(Debug, Clone, Serialize, Deserialize)]
     19 pub struct BotChatMessage {
     20    pub id: Option<u64>,
     21    #[serde(with = "datetime_format")]
     22    pub timestamp: DateTime<Utc>,
     23    pub username: String,
     24    pub content: String,
     25    pub message_type: MessageType,
     26    pub is_deleted: bool,
     27    #[serde(with = "datetime_option_format")]
     28    pub deleted_at: Option<DateTime<Utc>>,
     29    pub edit_history: Vec<String>,
     30 }
     31 
     32 /// Types of messages the bot can track
     33 #[derive(Debug, Clone, Serialize, Deserialize)]
     34 pub enum MessageType {
     35    Normal,
     36    PrivateMessage { to: String },
     37    System,
     38    Join,
     39    Leave,
     40    Kick { by: String, reason: Option<String> },
     41    Ban { by: String, reason: Option<String> },
     42 }
     43 
     44 /// User statistics tracked by the bot
     45 #[derive(Debug, Clone, Serialize, Deserialize)]
     46 pub struct UserStats {
     47    pub username: String,
     48    #[serde(with = "datetime_format")]
     49    pub first_seen: DateTime<Utc>,
     50    #[serde(with = "datetime_format")]
     51    pub last_seen: DateTime<Utc>,
     52    pub total_messages: u64,
     53    #[serde(with = "duration_secs")]
     54    pub total_time_online: Duration,
     55    pub kicks_received: u64,
     56    pub kicks_given: u64,
     57    pub bans_received: u64,
     58    pub bans_given: u64,
     59    pub warnings_received: u64,
     60    pub warnings_given: u64,
     61    pub session_starts: u64,
     62    pub favorite_words: HashMap<String, u64>,
     63    pub hourly_activity: [u64; 24], // Activity by hour of day
     64    pub daily_activity: HashMap<String, u64>, // Activity by date
     65 }
     66 
     67 mod duration_secs {
     68    use serde::{Deserialize, Deserializer, Serialize, Serializer};
     69    use std::time::Duration;
     70 
     71    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
     72    where
     73        S: Serializer,
     74    {
     75        duration.as_secs().serialize(serializer)
     76    }
     77 
     78    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
     79    where
     80        D: Deserializer<'de>,
     81    {
     82        let secs = u64::deserialize(deserializer)?;
     83        Ok(Duration::from_secs(secs))
     84    }
     85 }
     86 
     87 mod datetime_format {
     88    use chrono::{DateTime, Utc};
     89    use serde::{Deserialize, Deserializer, Serialize, Serializer};
     90 
     91    pub fn serialize<S>(datetime: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
     92    where
     93        S: Serializer,
     94    {
     95        datetime.timestamp().serialize(serializer)
     96    }
     97 
     98    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
     99    where
    100        D: Deserializer<'de>,
    101    {
    102        let timestamp = i64::deserialize(deserializer)?;
    103        DateTime::from_timestamp(timestamp, 0)
    104            .ok_or_else(|| serde::de::Error::custom("invalid timestamp"))
    105    }
    106 }
    107 
    108 mod datetime_option_format {
    109    use chrono::{DateTime, Utc};
    110    use serde::{Deserialize, Deserializer, Serialize, Serializer};
    111 
    112    pub fn serialize<S>(datetime: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
    113    where
    114        S: Serializer,
    115    {
    116        match datetime {
    117            Some(dt) => Some(dt.timestamp()).serialize(serializer),
    118            None => None::<i64>.serialize(serializer),
    119        }
    120    }
    121 
    122    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
    123    where
    124        D: Deserializer<'de>,
    125    {
    126        match Option::<i64>::deserialize(deserializer)? {
    127            Some(timestamp) => Ok(DateTime::from_timestamp(timestamp, 0)),
    128            None => Ok(None),
    129        }
    130    }
    131 }
    132 
    133 impl Default for UserStats {
    134    fn default() -> Self {
    135        Self {
    136            username: String::new(),
    137            first_seen: Utc::now(),
    138            last_seen: Utc::now(),
    139            total_messages: 0,
    140            total_time_online: Duration::new(0, 0),
    141            kicks_received: 0,
    142            kicks_given: 0,
    143            bans_received: 0,
    144            bans_given: 0,
    145            warnings_received: 0,
    146            warnings_given: 0,
    147            session_starts: 0,
    148            favorite_words: HashMap::new(),
    149            hourly_activity: [0; 24],
    150            daily_activity: HashMap::new(),
    151        }
    152    }
    153 }
    154 
    155 /// Bot command structure
    156 #[derive(Debug, Clone)]
    157 pub struct BotCommand {
    158    pub name: String,
    159    pub args: Vec<String>,
    160    pub requester: String,
    161    pub channel_context: Option<String>, // "members" for [M] channel, None for public
    162    pub is_member: bool,                 // True if requester is a member
    163 }
    164 
    165 /// Bot response types
    166 #[derive(Debug, Clone)]
    167 #[allow(dead_code)]
    168 pub enum BotResponse {
    169    PublicMessage(String),
    170    PrivateMessage { to: String, content: String },
    171    Action(BotAction),
    172    Error(String),
    173 }
    174 
    175 /// Actions the bot can perform
    176 #[derive(Debug, Clone)]
    177 #[allow(dead_code)]
    178 pub enum BotAction {
    179    Kick { username: String, reason: String },
    180    Ban { username: String, reason: String },
    181    Warn { username: String, message: String },
    182    SaveChatLog { filename: String },
    183    RestoreMessage { message_id: u64 },
    184 }
    185 
    186 /// Configuration for the bot
    187 #[derive(Debug, Clone, Serialize, Deserialize)]
    188 pub struct BotConfig {
    189    pub bot_name: String,
    190    pub data_directory: PathBuf,
    191    pub max_message_history: usize,
    192    pub auto_save_interval: Duration,
    193    pub enable_ai_integration: bool,
    194    pub admin_users: Vec<String>,
    195    pub moderator_users: Vec<String>,
    196    pub command_prefix: String,
    197    pub respond_to_mentions: bool,
    198    pub log_private_messages: bool,
    199    pub max_export_lines: usize,
    200 }
    201 
    202 impl Default for BotConfig {
    203    fn default() -> Self {
    204        Self {
    205            bot_name: "BotAssistant".to_string(),
    206            data_directory: PathBuf::from("bot_data"),
    207            max_message_history: 50000,
    208            auto_save_interval: Duration::from_secs(300), // 5 minutes
    209            enable_ai_integration: true,
    210            admin_users: Vec::new(),
    211            moderator_users: Vec::new(),
    212            command_prefix: "!".to_string(),
    213            respond_to_mentions: true,
    214            log_private_messages: false,
    215            max_export_lines: 10000,
    216        }
    217    }
    218 }
    219 
    220 /// Main bot system
    221 pub struct BotSystem {
    222    config: BotConfig,
    223    message_history: Arc<Mutex<Vec<BotChatMessage>>>,
    224    user_stats: Arc<Mutex<HashMap<String, UserStats>>>,
    225    current_users: Arc<Mutex<Users>>,
    226    ai_service: Option<Arc<AIService>>,
    227 
    228    tx: Sender<PostType>,
    229    running: Arc<Mutex<bool>>,
    230    last_save: Arc<Mutex<SystemTime>>,
    231 }
    232 
    233 impl BotSystem {
    234    /// Create a new bot system
    235    pub fn new(
    236        config: BotConfig,
    237        tx: Sender<PostType>,
    238        ai_service: Option<Arc<AIService>>,
    239        _runtime: Option<Arc<Runtime>>,
    240    ) -> Result<Self> {
    241        // Ensure data directory exists
    242        std::fs::create_dir_all(&config.data_directory)?;
    243 
    244        let bot = Self {
    245            config,
    246            message_history: Arc::new(Mutex::new(Vec::new())),
    247            user_stats: Arc::new(Mutex::new(HashMap::new())),
    248            current_users: Arc::new(Mutex::new(Users::default())),
    249            ai_service,
    250            tx,
    251            running: Arc::new(Mutex::new(false)),
    252            last_save: Arc::new(Mutex::new(SystemTime::now())),
    253        };
    254 
    255        // Load existing data
    256        bot.load_data()?;
    257 
    258        Ok(bot)
    259    }
    260 
    261    /// Start the bot system
    262    pub fn start(&self) -> Result<()> {
    263        {
    264            let mut running = self.running.lock().unwrap();
    265            if *running {
    266                return Err(anyhow!("Bot system is already running"));
    267            }
    268            *running = true;
    269        }
    270 
    271        info!("Starting bot system: {}", self.config.bot_name);
    272 
    273        // Start auto-save thread
    274        self.start_auto_save_thread();
    275 
    276        // Send startup message (non-blocking)
    277        let startup_msg = format!(
    278            "🤖 {} is now online! Type @{} help for available commands.",
    279            self.config.bot_name, self.config.bot_name
    280        );
    281        if let Err(e) = self
    282            .tx
    283            .try_send(PostType::Post(startup_msg, Some("0".to_string())))
    284        {
    285            warn!(
    286                "Could not send startup message (channel may be disconnected): {}",
    287                e
    288            );
    289        }
    290 
    291        Ok(())
    292    }
    293 
    294    /// Stop the bot system
    295    pub fn stop(&self) -> Result<()> {
    296        {
    297            let mut running = self.running.lock().unwrap();
    298            if !*running {
    299                return Ok(());
    300            }
    301            *running = false;
    302        }
    303 
    304        info!("Stopping bot system: {}", self.config.bot_name);
    305 
    306        // Save all data before stopping
    307        self.save_data()?;
    308 
    309        // Try to send shutdown message, but don't fail if channel is disconnected
    310        let shutdown_msg = format!("🤖 {} is going offline. Data saved.", self.config.bot_name);
    311        if let Err(e) = self
    312            .tx
    313            .try_send(PostType::Post(shutdown_msg, Some("0".to_string())))
    314        {
    315            // Log the error but don't fail the shutdown process
    316            warn!(
    317                "Could not send shutdown message (channel may be disconnected): {}",
    318                e
    319            );
    320        }
    321 
    322        Ok(())
    323    }
    324 
    325    /// Process a new message
    326    pub fn process_message(
    327        &self,
    328        username: &str,
    329        content: &str,
    330        message_type: MessageType,
    331        message_id: Option<u64>,
    332        channel_context: Option<&str>, // "members" for [M] channel, None for public
    333        is_member: bool,               // True if requester is a member
    334    ) -> Result<()> {
    335        let timestamp = Utc::now();
    336 
    337        // Create bot message record
    338        let bot_message = BotChatMessage {
    339            id: message_id,
    340            timestamp,
    341            username: username.to_string(),
    342            content: content.to_string(),
    343            message_type: message_type.clone(),
    344            is_deleted: false,
    345            deleted_at: None,
    346            edit_history: Vec::new(),
    347        };
    348 
    349        // Store message in history
    350        {
    351            let mut history = self.message_history.lock().unwrap();
    352            history.push(bot_message);
    353 
    354            // Limit history size
    355            if history.len() > self.config.max_message_history {
    356                let excess = history.len() - self.config.max_message_history;
    357                history.drain(0..excess);
    358            }
    359        }
    360 
    361        // Update user statistics
    362        self.update_user_stats(username, content, &message_type, timestamp)?;
    363 
    364        // Check for bot commands if mentioned and command is at start of message
    365        if self.config.respond_to_mentions && self.is_bot_mentioned_at_start(content) {
    366            info!(
    367                "Bot '{}' processing command from {}: {}",
    368                self.config.bot_name, username, content
    369            );
    370            self.handle_mention_commands(
    371                username,
    372                content,
    373                matches!(message_type, MessageType::PrivateMessage { .. }),
    374                channel_context,
    375                is_member,
    376            )?;
    377        }
    378 
    379        Ok(())
    380    }
    381 
    382    /// Process message deletion
    383    #[allow(dead_code)]
    384    pub fn process_message_deletion(&self, message_id: u64) -> Result<()> {
    385        let mut history = self.message_history.lock().unwrap();
    386 
    387        if let Some(message) = history.iter_mut().find(|m| m.id == Some(message_id)) {
    388            message.is_deleted = true;
    389            message.deleted_at = Some(Utc::now());
    390 
    391            info!("Bot recorded message deletion: ID {}", message_id);
    392        }
    393 
    394        Ok(())
    395    }
    396 
    397    /// Update current users list
    398    #[allow(dead_code)]
    399    pub fn update_users(&self, users: Users) -> Result<()> {
    400        *self.current_users.lock().unwrap() = users;
    401        Ok(())
    402    }
    403 
    404    /// Check if bot is mentioned at the start of content (not embedded)
    405    fn is_bot_mentioned_at_start(&self, content: &str) -> bool {
    406        let mention_pattern = format!("@{}", self.config.bot_name.to_lowercase());
    407        let binding = content.to_lowercase();
    408        let content_lower = binding.trim();
    409        content_lower.starts_with(&mention_pattern)
    410    }
    411 
    412    /// Handle commands when bot is mentioned
    413    fn handle_mention_commands(
    414        &self,
    415        requester: &str,
    416        content: &str,
    417        is_private: bool,
    418        channel_context: Option<&str>,
    419        is_member: bool,
    420    ) -> Result<()> {
    421        let commands =
    422            self.parse_commands(content, requester, is_private, channel_context, is_member)?;
    423 
    424        for command in commands {
    425            info!(
    426                "Bot '{}' executing command: '{}'",
    427                self.config.bot_name, command.name
    428            );
    429            match self.execute_command(&command) {
    430                Ok(response) => {
    431                    self.send_response_with_context(response, &command)?;
    432                }
    433                Err(e) => {
    434                    warn!(
    435                        "Bot '{}' command execution failed: {} - {}",
    436                        self.config.bot_name, command.name, e
    437                    );
    438                    let error_response = BotResponse::PrivateMessage {
    439                        to: requester.to_string(),
    440                        content: format!("Error executing {}: {}", command.name, e),
    441                    };
    442                    self.send_response_with_context(error_response, &command)?;
    443                }
    444            }
    445        }
    446 
    447        Ok(())
    448    }
    449 
    450    /// Parse commands from message content
    451    fn parse_commands(
    452        &self,
    453        content: &str,
    454        requester: &str,
    455        _is_private: bool,
    456        channel_context: Option<&str>,
    457        is_member: bool,
    458    ) -> Result<Vec<BotCommand>> {
    459        let mut commands = Vec::new();
    460 
    461        // Look for commands in the format: @botname command arg1 arg2 (at start of message only)
    462        let mention_pattern = format!("@{}", self.config.bot_name.to_lowercase());
    463        let binding = content.to_lowercase();
    464        let content_lower = binding.trim();
    465 
    466        if content_lower.starts_with(&mention_pattern) {
    467            let after_mention = &content[mention_pattern.len()..];
    468            let words: Vec<&str> = after_mention.split_whitespace().collect();
    469 
    470            if !words.is_empty() {
    471                let command_name = words[0].to_string();
    472                let args: Vec<String> = words[1..].iter().map(|&s| s.to_string()).collect();
    473 
    474                commands.push(BotCommand {
    475                    name: command_name,
    476                    args,
    477                    requester: requester.to_string(),
    478                    channel_context: channel_context.map(|s| s.to_string()),
    479                    is_member,
    480                });
    481            }
    482        }
    483 
    484        Ok(commands)
    485    }
    486 
    487    /// Execute a bot command
    488    fn execute_command(&self, command: &BotCommand) -> Result<BotResponse> {
    489        // Check permissions for moderation commands
    490        match command.name.to_lowercase().as_str() {
    491            "kick" | "ban" if !command.is_member => {
    492                return Ok(BotResponse::PrivateMessage {
    493                    to: command.requester.clone(),
    494                    content: "❌ Only members can use moderation commands".to_string(),
    495                });
    496            }
    497            _ => {}
    498        }
    499 
    500        match command.name.to_lowercase().as_str() {
    501            "help" => self.cmd_help(command),
    502            "stats" => self.cmd_stats(command),
    503            "recall" => self.cmd_recall(command),
    504            "search" => self.cmd_search(command),
    505            "export" => self.cmd_export(command),
    506            "restore" => self.cmd_restore(command),
    507            "users" => self.cmd_users(command),
    508            "top" => self.cmd_top(command),
    509            "history" => self.cmd_history(command),
    510            "summary" => self.cmd_summary(command),
    511            "status" => self.cmd_status(command),
    512            "purge" => self.cmd_purge(command),
    513            "kick" => self.cmd_kick(command),
    514            "ban" => self.cmd_ban(command),
    515            _ => Err(anyhow!("Unknown command: {}", command.name)),
    516        }
    517    }
    518 
    519    /// Help command
    520    fn cmd_help(&self, _command: &BotCommand) -> Result<BotResponse> {
    521        // Get AI status for help message
    522        let ai_status_note = if self.ai_service.is_some() {
    523            "\n\n🤖 **AI Features:**\nAdvanced AI commands are available via ChatOps (type `/help` in main chat).\nIf AI features are unavailable, it may be due to API quota limits."
    524        } else {
    525            "\n\n🤖 **AI Features:**\nAI integration is disabled. Advanced AI commands are not available."
    526        };
    527 
    528        let help_text = format!(
    529            "🤖 **{} Commands:**\n\n\
    530            **📊 Statistics & Info:**\n\
    531            • `@{} stats [username]` - View user statistics\n\
    532            • `@{} users` - List current online users\n\
    533            • `@{} top [messages|time|kicks]` - Top user rankings\n\
    534            • `@{} status` - Bot system status\n\n\
    535            **🔍 Search & Recall:**\n\
    536            • `@{} recall <timestamp>` - Find message by timestamp\n\
    537            • `@{} search <term>` - Search message history\n\
    538            • `@{} history <username> [count]` - User message history\n\n\
    539            **📋 Data Management:**\n\
    540            • `@{} export [username] [days]` - Export chat logs\n\
    541            • `@{} restore <message_id>` - Restore deleted message\n\
    542            • `@{} summary [hours]` - Chat activity summary\n\n\
    543            **🛠️ Admin Commands:**\n\
    544            • `@{} purge <username>` - Clear user data (admin only)\n\n\
    545            **⚖️ Moderation Commands (Members Only):**\n\
    546            • `@{} kick <username> [reason]` - Kick user from chat\n\
    547            • `@{} ban <username> [reason]` - Ban user from chat\n\n\
    548            ℹ️  **Note:** These core commands always work, even when AI services are unavailable.\n\
    549            Use `@{} help <command>` for detailed help on specific commands.{}",
    550            self.config.bot_name,
    551            self.config.bot_name,
    552            self.config.bot_name,
    553            self.config.bot_name,
    554            self.config.bot_name,
    555            self.config.bot_name,
    556            self.config.bot_name,
    557            self.config.bot_name,
    558            self.config.bot_name,
    559            self.config.bot_name,
    560            self.config.bot_name,
    561            self.config.bot_name,
    562            self.config.bot_name,
    563            self.config.bot_name,
    564            self.config.bot_name,
    565            ai_status_note
    566        );
    567 
    568        Ok(BotResponse::PublicMessage(help_text))
    569    }
    570 
    571    /// Stats command
    572    fn cmd_stats(&self, command: &BotCommand) -> Result<BotResponse> {
    573        let username = if command.args.is_empty() {
    574            &command.requester
    575        } else {
    576            &command.args[0]
    577        };
    578 
    579        let stats = self.user_stats.lock().unwrap();
    580        if let Some(user_stats) = stats.get(username) {
    581            let total_hours = user_stats.total_time_online.as_secs() / 3600;
    582            let avg_messages_per_day = if user_stats.session_starts > 0 {
    583                user_stats.total_messages / user_stats.session_starts.max(1)
    584            } else {
    585                0
    586            };
    587 
    588            let top_words: Vec<_> = user_stats
    589                .favorite_words
    590                .iter()
    591                .filter(|(word, _)| word.len() > 3) // Filter short words
    592                .collect();
    593            let mut top_words = top_words;
    594            top_words.sort_by(|a, b| b.1.cmp(a.1));
    595            let top_3_words: Vec<String> = top_words
    596                .iter()
    597                .take(3)
    598                .map(|(word, count)| format!("{} ({})", word, count))
    599                .collect();
    600 
    601            let response = format!(
    602                "📊 **Stats for {}:**\n\
    603                • Messages: {} (avg {}/session)\n\
    604                • Time Online: {} hours\n\
    605                • Sessions: {}\n\
    606                • First Seen: {}\n\
    607                • Last Seen: {}\n\
    608                • Kicks: {} received, {} given\n\
    609                • Bans: {} received, {} given\n\
    610                • Top Words: {}",
    611                username,
    612                user_stats.total_messages,
    613                avg_messages_per_day,
    614                total_hours,
    615                user_stats.session_starts,
    616                user_stats.first_seen.format("%Y-%m-%d %H:%M UTC"),
    617                user_stats.last_seen.format("%Y-%m-%d %H:%M UTC"),
    618                user_stats.kicks_received,
    619                user_stats.kicks_given,
    620                user_stats.bans_received,
    621                user_stats.bans_given,
    622                if top_3_words.is_empty() {
    623                    "None".to_string()
    624                } else {
    625                    top_3_words.join(", ")
    626                }
    627            );
    628 
    629            Ok(BotResponse::PrivateMessage {
    630                to: command.requester.clone(),
    631                content: response,
    632            })
    633        } else {
    634            Ok(BotResponse::PrivateMessage {
    635                to: command.requester.clone(),
    636                content: format!("❌ No statistics found for user '{}'", username),
    637            })
    638        }
    639    }
    640 
    641    /// Recall command - find message by timestamp
    642    fn cmd_recall(&self, command: &BotCommand) -> Result<BotResponse> {
    643        if command.args.is_empty() {
    644            return Ok(BotResponse::PrivateMessage {
    645                to: command.requester.clone(),
    646                content: "❌ Usage: @{} recall <timestamp> (format: YYYY-MM-DD HH:MM or 'HH:MM' for today)".to_string(),
    647            });
    648        }
    649 
    650        let timestamp_str = command.args.join(" ");
    651        let target_time = self.parse_timestamp(&timestamp_str)?;
    652 
    653        let history = self.message_history.lock().unwrap();
    654        let mut closest_messages: Vec<_> = history
    655            .iter()
    656            .filter(|msg| !msg.is_deleted)
    657            .map(|msg| {
    658                let diff = if msg.timestamp > target_time {
    659                    msg.timestamp.signed_duration_since(target_time)
    660                } else {
    661                    target_time.signed_duration_since(msg.timestamp)
    662                };
    663                (msg, diff.num_seconds().abs())
    664            })
    665            .collect();
    666 
    667        closest_messages.sort_by_key(|(_, diff)| *diff);
    668 
    669        if let Some((message, diff_seconds)) = closest_messages.first() {
    670            let response = format!(
    671                "🔍 **Closest message to {}:**\n\
    672                **[{}]** {}: {}\n\
    673                *(Time difference: {} seconds)*",
    674                timestamp_str,
    675                message.timestamp.format("%H:%M:%S"),
    676                message.username,
    677                message.content,
    678                diff_seconds
    679            );
    680 
    681            Ok(BotResponse::PrivateMessage {
    682                to: command.requester.clone(),
    683                content: response,
    684            })
    685        } else {
    686            Ok(BotResponse::PrivateMessage {
    687                to: command.requester.clone(),
    688                content: "❌ No messages found in history".to_string(),
    689            })
    690        }
    691    }
    692 
    693    /// Search command
    694    fn cmd_search(&self, command: &BotCommand) -> Result<BotResponse> {
    695        if command.args.is_empty() {
    696            return Ok(BotResponse::PrivateMessage {
    697                to: command.requester.clone(),
    698                content: "❌ Usage: @{} search <search term>".to_string(),
    699            });
    700        }
    701 
    702        let search_term = command.args.join(" ").to_lowercase();
    703        let history = self.message_history.lock().unwrap();
    704 
    705        let matches: Vec<_> = history
    706            .iter()
    707            .rev() // Most recent first
    708            .filter(|msg| !msg.is_deleted && msg.content.to_lowercase().contains(&search_term))
    709            .take(5) // Limit to 5 results
    710            .collect();
    711 
    712        if matches.is_empty() {
    713            return Ok(BotResponse::PrivateMessage {
    714                to: command.requester.clone(),
    715                content: format!("❌ No messages found containing '{}'", search_term),
    716            });
    717        }
    718 
    719        let mut response = format!("🔍 **Search results for '{}':**\n", search_term);
    720        for (i, message) in matches.iter().enumerate() {
    721            response.push_str(&format!(
    722                "{}. **[{}]** {}: {}\n",
    723                i + 1,
    724                message.timestamp.format("%m-%d %H:%M"),
    725                message.username,
    726                if message.content.len() > 100 {
    727                    format!("{}...", &message.content[..100])
    728                } else {
    729                    message.content.clone()
    730                }
    731            ));
    732        }
    733 
    734        Ok(BotResponse::PrivateMessage {
    735            to: command.requester.clone(),
    736            content: response,
    737        })
    738    }
    739 
    740    /// Export command
    741    fn cmd_export(&self, command: &BotCommand) -> Result<BotResponse> {
    742        let (username_filter, days_back) = if command.args.len() >= 2 {
    743            (
    744                Some(command.args[0].clone()),
    745                command.args[1].parse::<i64>().unwrap_or(1),
    746            )
    747        } else if command.args.len() == 1 {
    748            if let Ok(days) = command.args[0].parse::<i64>() {
    749                (None, days)
    750            } else {
    751                (Some(command.args[0].clone()), 1)
    752            }
    753        } else {
    754            (None, 1)
    755        };
    756 
    757        let cutoff_time = Utc::now() - chrono::Duration::days(days_back);
    758        let history = self.message_history.lock().unwrap();
    759 
    760        let messages: Vec<_> = history
    761            .iter()
    762            .filter(|msg| {
    763                msg.timestamp >= cutoff_time
    764                    && !msg.is_deleted
    765                    && username_filter
    766                        .as_ref()
    767                        .is_none_or(|filter| &msg.username == filter)
    768            })
    769            .take(self.config.max_export_lines)
    770            .collect();
    771 
    772        if messages.is_empty() {
    773            return Ok(BotResponse::PrivateMessage {
    774                to: command.requester.clone(),
    775                content: "❌ No messages found for export criteria".to_string(),
    776            });
    777        }
    778 
    779        // Generate filename
    780        let filename = format!(
    781            "chat_export_{}_{}.txt",
    782            username_filter.as_deref().unwrap_or("all"),
    783            Utc::now().format("%Y%m%d_%H%M%S")
    784        );
    785 
    786        let filepath = self.config.data_directory.join(&filename);
    787        let mut file = File::create(&filepath)?;
    788 
    789        writeln!(
    790            file,
    791            "Chat Export Generated: {}",
    792            Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
    793        )?;
    794        writeln!(
    795            file,
    796            "Filter: {}",
    797            username_filter.as_deref().unwrap_or("All users")
    798        )?;
    799        writeln!(file, "Time Range: {} days back", days_back)?;
    800        writeln!(file, "Total Messages: {}\n", messages.len())?;
    801        writeln!(file, "{:-<80}", "")?;
    802 
    803        for message in &messages {
    804            writeln!(
    805                file,
    806                "[{}] {}: {}",
    807                message.timestamp.format("%Y-%m-%d %H:%M:%S"),
    808                message.username,
    809                message.content
    810            )?;
    811        }
    812 
    813        let response = format!(
    814            "✅ **Export completed!**\n\
    815            • File: {}\n\
    816            • Messages: {}\n\
    817            • Time Range: {} days\n\
    818            • Filter: {}",
    819            filename,
    820            messages.len(),
    821            days_back,
    822            username_filter.as_deref().unwrap_or("All users")
    823        );
    824 
    825        Ok(BotResponse::PrivateMessage {
    826            to: command.requester.clone(),
    827            content: response,
    828        })
    829    }
    830 
    831    /// Restore command
    832    fn cmd_restore(&self, command: &BotCommand) -> Result<BotResponse> {
    833        if command.args.is_empty() {
    834            return Ok(BotResponse::PrivateMessage {
    835                to: command.requester.clone(),
    836                content: "❌ Usage: @{} restore <message_id>".to_string(),
    837            });
    838        }
    839 
    840        let message_id: u64 = command.args[0]
    841            .parse()
    842            .map_err(|_| anyhow!("Invalid message ID"))?;
    843 
    844        let mut history = self.message_history.lock().unwrap();
    845        if let Some(message) = history.iter_mut().find(|m| m.id == Some(message_id)) {
    846            if message.is_deleted {
    847                let restored_content = message.content.clone();
    848                let original_author = message.username.clone();
    849                let original_time = message.timestamp;
    850 
    851                // Mark as not deleted
    852                message.is_deleted = false;
    853                message.deleted_at = None;
    854 
    855                // Send the restored message back to chat
    856                let restore_msg = format!(
    857                    "🔄 **Message Restored by {}:**\n[{}] {}: {}",
    858                    command.requester,
    859                    original_time.format("%H:%M:%S"),
    860                    original_author,
    861                    restored_content
    862                );
    863 
    864                return Ok(BotResponse::PublicMessage(restore_msg));
    865            } else {
    866                return Ok(BotResponse::PrivateMessage {
    867                    to: command.requester.clone(),
    868                    content: format!("❌ Message {} was not deleted", message_id),
    869                });
    870            }
    871        }
    872 
    873        Ok(BotResponse::PrivateMessage {
    874            to: command.requester.clone(),
    875            content: format!("❌ Message {} not found in history", message_id),
    876        })
    877    }
    878 
    879    /// Users command
    880    fn cmd_users(&self, command: &BotCommand) -> Result<BotResponse> {
    881        let users = self.current_users.lock().unwrap();
    882        let mut response = "👥 **Current Online Users:**\n\n".to_string();
    883 
    884        if !users.admin.is_empty() {
    885            response.push_str("**Admins:** ");
    886            let admin_names: Vec<String> =
    887                users.admin.iter().map(|(_, name)| name.clone()).collect();
    888            response.push_str(&admin_names.join(", "));
    889            response.push('\n');
    890        }
    891 
    892        if !users.staff.is_empty() {
    893            response.push_str("**Staff:** ");
    894            let staff_names: Vec<String> =
    895                users.staff.iter().map(|(_, name)| name.clone()).collect();
    896            response.push_str(&staff_names.join(", "));
    897            response.push('\n');
    898        }
    899 
    900        if !users.members.is_empty() {
    901            response.push_str("**Members:** ");
    902            let member_names: Vec<String> =
    903                users.members.iter().map(|(_, name)| name.clone()).collect();
    904            response.push_str(&member_names.join(", "));
    905            response.push('\n');
    906        }
    907 
    908        if !users.guests.is_empty() {
    909            response.push_str("**Guests:** ");
    910            let guest_names: Vec<String> =
    911                users.guests.iter().map(|(_, name)| name.clone()).collect();
    912            response.push_str(&guest_names.join(", "));
    913            response.push('\n');
    914        }
    915 
    916        let total_users =
    917            users.admin.len() + users.staff.len() + users.members.len() + users.guests.len();
    918        response.push_str(&format!("\n**Total:** {} users online", total_users));
    919 
    920        Ok(BotResponse::PrivateMessage {
    921            to: command.requester.clone(),
    922            content: response,
    923        })
    924    }
    925 
    926    /// Top command
    927    fn cmd_top(&self, command: &BotCommand) -> Result<BotResponse> {
    928        let category = command
    929            .args
    930            .first()
    931            .map(|s| s.as_str())
    932            .unwrap_or("messages");
    933        let stats = self.user_stats.lock().unwrap();
    934 
    935        let mut users: Vec<_> = stats.values().collect();
    936 
    937        match category {
    938            "messages" | "msgs" => {
    939                users.sort_by(|a, b| b.total_messages.cmp(&a.total_messages));
    940                let response = self.format_top_list("Most Active (Messages)", &users, |u| {
    941                    u.total_messages.to_string()
    942                });
    943                Ok(BotResponse::PrivateMessage {
    944                    to: command.requester.clone(),
    945                    content: response,
    946                })
    947            }
    948            "time" | "online" => {
    949                users.sort_by(|a, b| b.total_time_online.cmp(&a.total_time_online));
    950                let response = self.format_top_list("Most Time Online", &users, |u| {
    951                    format!("{:.1}h", u.total_time_online.as_secs() as f64 / 3600.0)
    952                });
    953                Ok(BotResponse::PrivateMessage {
    954                    to: command.requester.clone(),
    955                    content: response,
    956                })
    957            }
    958            "kicks" => {
    959                users.sort_by(|a, b| b.kicks_given.cmp(&a.kicks_given));
    960                let response =
    961                    self.format_top_list("Most Kicks Given", &users, |u| u.kicks_given.to_string());
    962                Ok(BotResponse::PrivateMessage {
    963                    to: command.requester.clone(),
    964                    content: response,
    965                })
    966            }
    967            _ => Ok(BotResponse::PrivateMessage {
    968                to: command.requester.clone(),
    969                content: "❌ Usage: @{} top [messages|time|kicks]".to_string(),
    970            }),
    971        }
    972    }
    973 
    974    /// History command
    975    fn cmd_history(&self, command: &BotCommand) -> Result<BotResponse> {
    976        if command.args.is_empty() {
    977            return Ok(BotResponse::PrivateMessage {
    978                to: command.requester.clone(),
    979                content: "❌ Usage: @{} history <username> [count]".to_string(),
    980            });
    981        }
    982 
    983        let username = &command.args[0];
    984        let count: usize = command
    985            .args
    986            .get(1)
    987            .and_then(|s| s.parse().ok())
    988            .unwrap_or(10);
    989 
    990        let history = self.message_history.lock().unwrap();
    991        let user_messages: Vec<_> = history
    992            .iter()
    993            .rev()
    994            .filter(|msg| &msg.username == username && !msg.is_deleted)
    995            .take(count)
    996            .collect();
    997 
    998        if user_messages.is_empty() {
    999            return Ok(BotResponse::PrivateMessage {
   1000                to: command.requester.clone(),
   1001                content: format!("❌ No message history found for '{}'", username),
   1002            });
   1003        }
   1004 
   1005        let mut response = format!(
   1006            "📜 **Recent messages from {} (last {}):**\n",
   1007            username,
   1008            user_messages.len()
   1009        );
   1010        for (i, message) in user_messages.iter().enumerate() {
   1011            response.push_str(&format!(
   1012                "{}. **[{}]** {}\n",
   1013                i + 1,
   1014                message.timestamp.format("%m-%d %H:%M"),
   1015                if message.content.len() > 80 {
   1016                    format!("{}...", &message.content[..80])
   1017                } else {
   1018                    message.content.clone()
   1019                }
   1020            ));
   1021        }
   1022 
   1023        Ok(BotResponse::PrivateMessage {
   1024            to: command.requester.clone(),
   1025            content: response,
   1026        })
   1027    }
   1028 
   1029    /// Summary command
   1030    fn cmd_summary(&self, command: &BotCommand) -> Result<BotResponse> {
   1031        let hours_back: i64 = command
   1032            .args
   1033            .first()
   1034            .and_then(|s| s.parse().ok())
   1035            .unwrap_or(24);
   1036 
   1037        let cutoff_time = Utc::now() - chrono::Duration::hours(hours_back);
   1038        let history = self.message_history.lock().unwrap();
   1039 
   1040        let recent_messages: Vec<_> = history
   1041            .iter()
   1042            .filter(|msg| msg.timestamp >= cutoff_time && !msg.is_deleted)
   1043            .collect();
   1044 
   1045        if recent_messages.is_empty() {
   1046            return Ok(BotResponse::PrivateMessage {
   1047                to: command.requester.clone(),
   1048                content: format!("❌ No messages found in the last {} hours", hours_back),
   1049            });
   1050        }
   1051 
   1052        // Analyze activity
   1053        let total_messages = recent_messages.len();
   1054        let unique_users: std::collections::HashSet<_> =
   1055            recent_messages.iter().map(|msg| &msg.username).collect();
   1056        let user_count = unique_users.len();
   1057 
   1058        // Most active user
   1059        let mut user_message_counts: HashMap<&String, usize> = HashMap::new();
   1060        for message in &recent_messages {
   1061            *user_message_counts.entry(&message.username).or_insert(0) += 1;
   1062        }
   1063 
   1064        let most_active = user_message_counts
   1065            .iter()
   1066            .max_by_key(|(_, count)| *count)
   1067            .map(|(user, count)| format!("{} ({})", user, count))
   1068            .unwrap_or_else(|| "None".to_string());
   1069 
   1070        // Activity by hour
   1071        let mut hourly_activity: [usize; 24] = [0; 24];
   1072        for message in &recent_messages {
   1073            let hour = message.timestamp.hour() as usize;
   1074            hourly_activity[hour] += 1;
   1075        }
   1076 
   1077        let peak_hour = hourly_activity
   1078            .iter()
   1079            .enumerate()
   1080            .max_by_key(|(_, count)| *count)
   1081            .map(|(hour, count)| format!("{}:00 ({} msgs)", hour, count))
   1082            .unwrap_or_else(|| "None".to_string());
   1083 
   1084        let response = format!(
   1085            "📊 **Chat Summary (last {} hours):**\n\
   1086            • Total Messages: {}\n\
   1087            • Active Users: {}\n\
   1088            • Most Active User: {}\n\
   1089            • Peak Hour: {}\n\
   1090            • Messages per Hour: {:.1}",
   1091            hours_back,
   1092            total_messages,
   1093            user_count,
   1094            most_active,
   1095            peak_hour,
   1096            total_messages as f64 / hours_back as f64
   1097        );
   1098 
   1099        Ok(BotResponse::PrivateMessage {
   1100            to: command.requester.clone(),
   1101            content: response,
   1102        })
   1103    }
   1104 
   1105    /// Status command
   1106    fn cmd_status(&self, _command: &BotCommand) -> Result<BotResponse> {
   1107        let history_count = self.message_history.lock().unwrap().len();
   1108        let user_count = self.user_stats.lock().unwrap().len();
   1109        let uptime = SystemTime::now()
   1110            .duration_since(*self.last_save.lock().unwrap())
   1111            .unwrap_or_default();
   1112 
   1113        let is_running = *self.running.lock().unwrap();
   1114 
   1115        // Get AI configuration status (no actual API calls)
   1116        let ai_status = if self.ai_service.is_some() {
   1117            "✅ Configured"
   1118        } else {
   1119            "❌ Disabled"
   1120        };
   1121 
   1122        let response = format!(
   1123            "🤖 **{} Status:**\n\
   1124            • Status: {}\n\
   1125            • Messages Tracked: {}\n\
   1126            • Users Tracked: {}\n\
   1127            • Last Save: {:.1} minutes ago\n\
   1128            • Data Directory: {}\n\
   1129            • AI Integration: {}\n\
   1130            • Max History: {}\n\
   1131            \n\
   1132            ℹ️  **Available Commands:**\n\
   1133            Basic commands (always work): help, stats, recall, export, search, history, top, users, restore, status\n\
   1134            Admin commands: purge\n\
   1135            AI commands: Available via ChatOps when AI is functional",
   1136            self.config.bot_name,
   1137            if is_running {
   1138                "🟢 Online"
   1139            } else {
   1140                "🔴 Offline"
   1141            },
   1142            history_count,
   1143            user_count,
   1144            uptime.as_secs() as f64 / 60.0,
   1145            self.config.data_directory.display(),
   1146            ai_status,
   1147            self.config.max_message_history
   1148        );
   1149 
   1150        Ok(BotResponse::PublicMessage(response))
   1151    }
   1152 
   1153    /// Purge command (admin only)
   1154    fn cmd_purge(&self, command: &BotCommand) -> Result<BotResponse> {
   1155        if !self.is_admin(&command.requester) {
   1156            return Ok(BotResponse::PrivateMessage {
   1157                to: command.requester.clone(),
   1158                content: "❌ Admin access required for this command".to_string(),
   1159            });
   1160        }
   1161 
   1162        if command.args.is_empty() {
   1163            return Ok(BotResponse::PrivateMessage {
   1164                to: command.requester.clone(),
   1165                content: "❌ Usage: @{} purge <username>".to_string(),
   1166            });
   1167        }
   1168 
   1169        let username = &command.args[0];
   1170 
   1171        // Remove from user stats
   1172        let mut stats = self.user_stats.lock().unwrap();
   1173        if stats.remove(username).is_some() {
   1174            drop(stats);
   1175 
   1176            // Remove from message history
   1177            let mut history = self.message_history.lock().unwrap();
   1178            history.retain(|msg| msg.username != *username);
   1179 
   1180            let response = format!(
   1181                "✅ **Purged all data for user '{}'**\n\
   1182                • Removed user statistics\n\
   1183                • Removed message history\n\
   1184                • Action performed by: {}",
   1185                username, command.requester
   1186            );
   1187 
   1188            // Save data after purge
   1189            if let Err(e) = self.save_data() {
   1190                warn!("Failed to save data after purge: {}", e);
   1191            }
   1192 
   1193            Ok(BotResponse::PublicMessage(response))
   1194        } else {
   1195            Ok(BotResponse::PrivateMessage {
   1196                to: command.requester.clone(),
   1197                content: format!("❌ No data found for user '{}'", username),
   1198            })
   1199        }
   1200    }
   1201 
   1202    /// Helper function to format top lists
   1203    fn format_top_list<F>(&self, title: &str, users: &[&UserStats], value_fn: F) -> String
   1204    where
   1205        F: Fn(&UserStats) -> String,
   1206    {
   1207        let mut response = format!("🏆 **{}:**\n", title);
   1208 
   1209        for (i, user) in users.iter().take(10).enumerate() {
   1210            let value = value_fn(user);
   1211            response.push_str(&format!("{}. {} - {}\n", i + 1, user.username, value));
   1212        }
   1213 
   1214        if users.is_empty() {
   1215            response.push_str("No data available yet.");
   1216        }
   1217 
   1218        response
   1219    }
   1220 
   1221    /// Parse timestamp string
   1222    fn parse_timestamp(&self, timestamp_str: &str) -> Result<DateTime<Utc>> {
   1223        let now = Utc::now();
   1224 
   1225        // Try parsing as HH:MM for today
   1226        if let Ok(naive_time) = chrono::NaiveTime::parse_from_str(timestamp_str, "%H:%M") {
   1227            let today = now.date_naive();
   1228            let naive_datetime = today.and_time(naive_time);
   1229            return Ok(naive_datetime.and_utc());
   1230        }
   1231 
   1232        // Try parsing as YYYY-MM-DD HH:MM
   1233        if let Ok(naive_datetime) =
   1234            chrono::NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%d %H:%M")
   1235        {
   1236            return Ok(naive_datetime.and_utc());
   1237        }
   1238 
   1239        // Try parsing as MM-DD HH:MM (current year)
   1240        if let Ok(naive_datetime) = chrono::NaiveDateTime::parse_from_str(
   1241            &format!("{}-{}", now.year(), timestamp_str),
   1242            "%Y-%m-%d %H:%M",
   1243        ) {
   1244            return Ok(naive_datetime.and_utc());
   1245        }
   1246 
   1247        Err(anyhow!(
   1248            "Invalid timestamp format. Use 'HH:MM', 'MM-DD HH:MM', or 'YYYY-MM-DD HH:MM'"
   1249        ))
   1250    }
   1251 
   1252    /// Check if user is admin
   1253    fn is_admin(&self, username: &str) -> bool {
   1254        self.config.admin_users.contains(&username.to_string())
   1255    }
   1256 
   1257    /// Kick command (members only)
   1258    fn cmd_kick(&self, command: &BotCommand) -> Result<BotResponse> {
   1259        if command.args.is_empty() {
   1260            return Ok(BotResponse::PrivateMessage {
   1261                to: command.requester.clone(),
   1262                content: format!(
   1263                    "❌ Usage: @{} kick <username> [reason]",
   1264                    self.config.bot_name
   1265                ),
   1266            });
   1267        }
   1268 
   1269        let username = &command.args[0];
   1270        let reason = if command.args.len() > 1 {
   1271            command.args[1..].join(" ")
   1272        } else {
   1273            "No reason provided".to_string()
   1274        };
   1275 
   1276        // Protect Dasho from being kicked
   1277        if username.to_lowercase() == "dasho" {
   1278            return Ok(BotResponse::PrivateMessage {
   1279                to: command.requester.clone(),
   1280                content: "❌ Cannot kick Dasho - protected user".to_string(),
   1281            });
   1282        }
   1283 
   1284        Ok(BotResponse::Action(BotAction::Kick {
   1285            username: username.clone(),
   1286            reason,
   1287        }))
   1288    }
   1289 
   1290    /// Ban command (members only)
   1291    fn cmd_ban(&self, command: &BotCommand) -> Result<BotResponse> {
   1292        if command.args.is_empty() {
   1293            return Ok(BotResponse::PrivateMessage {
   1294                to: command.requester.clone(),
   1295                content: format!(
   1296                    "❌ Usage: @{} ban <username> [reason]",
   1297                    self.config.bot_name
   1298                ),
   1299            });
   1300        }
   1301 
   1302        let username = &command.args[0];
   1303        let reason = if command.args.len() > 1 {
   1304            command.args[1..].join(" ")
   1305        } else {
   1306            "No reason provided".to_string()
   1307        };
   1308 
   1309        // Protect Dasho from being banned
   1310        if username.to_lowercase().contains("dasho") {
   1311            return Ok(BotResponse::PrivateMessage {
   1312                to: command.requester.clone(),
   1313                content: "❌ Cannot ban Dasho - protected user".to_string(),
   1314            });
   1315        }
   1316 
   1317        Ok(BotResponse::Action(BotAction::Ban {
   1318            username: username.clone(),
   1319            reason,
   1320        }))
   1321    }
   1322 
   1323    /// Send bot response with context-aware channel selection
   1324    fn send_response_with_context(
   1325        &self,
   1326        response: BotResponse,
   1327        command: &BotCommand,
   1328    ) -> Result<()> {
   1329        match response {
   1330            BotResponse::PublicMessage(content) => {
   1331                // Reply in the same channel as the command came from
   1332                let target = match command.channel_context.as_deref() {
   1333                    Some("members") => {
   1334                        log::info!("Bot '{}' responding in [M] channel", self.config.bot_name);
   1335                        Some(crate::SEND_TO_MEMBERS.to_string())
   1336                    },
   1337                    Some("staff") => {
   1338                        log::info!("Bot '{}' responding in [S] channel", self.config.bot_name);
   1339                        Some(crate::SEND_TO_STAFFS.to_string())
   1340                    },
   1341                    Some("admin") => {
   1342                        log::info!("Bot '{}' responding in [A] channel", self.config.bot_name);
   1343                        Some(crate::SEND_TO_ADMINS.to_string())
   1344                    },
   1345                    _ => {
   1346                        log::info!("Bot '{}' responding in main chat (context: {:?})", 
   1347                            self.config.bot_name, command.channel_context);
   1348                        None // Main chat (public channel)
   1349                    }
   1350                };
   1351 
   1352                if let Err(e) = self.tx.try_send(PostType::Post(content, target)) {
   1353                    warn!(
   1354                        "Bot '{}' failed to send public message: {}",
   1355                        self.config.bot_name, e
   1356                    );
   1357                }
   1358            }
   1359            BotResponse::PrivateMessage { to, content } => {
   1360                if let Err(e) = self.tx.try_send(PostType::PM(to, content)) {
   1361                    warn!(
   1362                        "Bot '{}' failed to send private message: {}",
   1363                        self.config.bot_name, e
   1364                    );
   1365                }
   1366            }
   1367            BotResponse::Action(action) => {
   1368                match action {
   1369                    BotAction::Kick { username, reason } => {
   1370                        if let Err(e) = self.tx.try_send(PostType::Kick(reason, username)) {
   1371                            warn!(
   1372                                "Failed to send kick action (channel may be disconnected): {}",
   1373                                e
   1374                            );
   1375                        }
   1376                    }
   1377                    BotAction::Ban {
   1378                        username,
   1379                        reason: _,
   1380                    } => {
   1381                        // Note: Implement ban action based on BHCLI's ban system
   1382                        let ban_msg = format!("/ban {}", username);
   1383                        if let Err(e) = self
   1384                            .tx
   1385                            .try_send(PostType::Post(ban_msg, Some("0".to_string())))
   1386                        {
   1387                            warn!(
   1388                                "Failed to send ban command (channel may be disconnected): {}",
   1389                                e
   1390                            );
   1391                        }
   1392                    }
   1393                    BotAction::Warn { username, message } => {
   1394                        let warn_msg = format!("⚠️ @{}: {}", username, message);
   1395                        if let Err(e) = self
   1396                            .tx
   1397                            .try_send(PostType::Post(warn_msg, Some("0".to_string())))
   1398                        {
   1399                            warn!(
   1400                                "Failed to send warning message (channel may be disconnected): {}",
   1401                                e
   1402                            );
   1403                        }
   1404                    }
   1405                    BotAction::SaveChatLog { filename: _ } => {
   1406                        // Handled by export command
   1407                    }
   1408                    BotAction::RestoreMessage { message_id: _ } => {
   1409                        // Handled by restore command
   1410                    }
   1411                }
   1412            }
   1413            BotResponse::Error(error) => {
   1414                error!("Bot error: {}", error);
   1415            }
   1416        }
   1417        Ok(())
   1418    }
   1419 
   1420    /// Update user statistics
   1421    fn update_user_stats(
   1422        &self,
   1423        username: &str,
   1424        content: &str,
   1425        message_type: &MessageType,
   1426        timestamp: DateTime<Utc>,
   1427    ) -> Result<()> {
   1428        let mut stats = self.user_stats.lock().unwrap();
   1429        let user_stats = stats.entry(username.to_string()).or_default();
   1430 
   1431        // Update basic stats
   1432        if user_stats.username.is_empty() {
   1433            user_stats.username = username.to_string();
   1434            user_stats.first_seen = timestamp;
   1435        }
   1436        user_stats.last_seen = timestamp;
   1437        user_stats.total_messages += 1;
   1438 
   1439        // Update hourly activity
   1440        let hour = timestamp.hour() as usize;
   1441        if hour < 24 {
   1442            user_stats.hourly_activity[hour] += 1;
   1443        }
   1444 
   1445        // Update daily activity
   1446        let date_key = timestamp.format("%Y-%m-%d").to_string();
   1447        *user_stats.daily_activity.entry(date_key).or_insert(0) += 1;
   1448 
   1449        // Update word frequency
   1450        let words: Vec<&str> = content.split_whitespace().collect();
   1451        for word in words {
   1452            let clean_word = word
   1453                .to_lowercase()
   1454                .chars()
   1455                .filter(|c| c.is_alphabetic())
   1456                .collect::<String>();
   1457 
   1458            if clean_word.len() > 3 {
   1459                *user_stats.favorite_words.entry(clean_word).or_insert(0) += 1;
   1460            }
   1461        }
   1462 
   1463        // Update message type specific stats
   1464        match message_type {
   1465            MessageType::Kick { by, .. } => {
   1466                if by == username {
   1467                    user_stats.kicks_given += 1;
   1468                } else {
   1469                    user_stats.kicks_received += 1;
   1470                }
   1471            }
   1472            MessageType::Ban { by, .. } => {
   1473                if by == username {
   1474                    user_stats.bans_given += 1;
   1475                } else {
   1476                    user_stats.bans_received += 1;
   1477                }
   1478            }
   1479            MessageType::Join => {
   1480                user_stats.session_starts += 1;
   1481            }
   1482            _ => {}
   1483        }
   1484 
   1485        Ok(())
   1486    }
   1487 
   1488    /// Start auto-save thread
   1489    fn start_auto_save_thread(&self) {
   1490        let message_history = Arc::clone(&self.message_history);
   1491        let user_stats = Arc::clone(&self.user_stats);
   1492        let running = Arc::clone(&self.running);
   1493        let last_save = Arc::clone(&self.last_save);
   1494        let data_dir = self.config.data_directory.clone();
   1495        let save_interval = self.config.auto_save_interval;
   1496 
   1497        thread::spawn(move || {
   1498            while *running.lock().unwrap() {
   1499                thread::sleep(save_interval);
   1500 
   1501                if let Err(e) = Self::save_data_to_disk(&message_history, &user_stats, &data_dir) {
   1502                    error!("Auto-save failed: {}", e);
   1503                } else {
   1504                    *last_save.lock().unwrap() = SystemTime::now();
   1505                }
   1506            }
   1507        });
   1508    }
   1509 
   1510    /// Save data to disk
   1511    fn save_data_to_disk(
   1512        message_history: &Arc<Mutex<Vec<BotChatMessage>>>,
   1513        user_stats: &Arc<Mutex<HashMap<String, UserStats>>>,
   1514        data_dir: &Path,
   1515    ) -> Result<()> {
   1516        // Save message history
   1517        let history_file = data_dir.join("message_history.json");
   1518        let history = message_history.lock().unwrap();
   1519        let history_json = serde_json::to_string_pretty(&*history)?;
   1520        std::fs::write(history_file, history_json)?;
   1521 
   1522        // Save user stats
   1523        let stats_file = data_dir.join("user_stats.json");
   1524        let stats = user_stats.lock().unwrap();
   1525        let stats_json = serde_json::to_string_pretty(&*stats)?;
   1526        std::fs::write(stats_file, stats_json)?;
   1527 
   1528        Ok(())
   1529    }
   1530 
   1531    /// Save all data
   1532    pub fn save_data(&self) -> Result<()> {
   1533        Self::save_data_to_disk(
   1534            &self.message_history,
   1535            &self.user_stats,
   1536            &self.config.data_directory,
   1537        )
   1538    }
   1539 
   1540    /// Load existing data
   1541    fn load_data(&self) -> Result<()> {
   1542        // Load message history
   1543        let history_file = self.config.data_directory.join("message_history.json");
   1544        if history_file.exists() {
   1545            let history_json = std::fs::read_to_string(history_file)?;
   1546            if let Ok(history) = serde_json::from_str::<Vec<BotChatMessage>>(&history_json) {
   1547                *self.message_history.lock().unwrap() = history;
   1548                info!(
   1549                    "Loaded {} messages from history",
   1550                    self.message_history.lock().unwrap().len()
   1551                );
   1552            }
   1553        }
   1554 
   1555        // Load user stats
   1556        let stats_file = self.config.data_directory.join("user_stats.json");
   1557        if stats_file.exists() {
   1558            let stats_json = std::fs::read_to_string(stats_file)?;
   1559            if let Ok(stats) = serde_json::from_str::<HashMap<String, UserStats>>(&stats_json) {
   1560                *self.user_stats.lock().unwrap() = stats;
   1561                info!(
   1562                    "Loaded {} user statistics",
   1563                    self.user_stats.lock().unwrap().len()
   1564                );
   1565            }
   1566        }
   1567 
   1568        Ok(())
   1569    }
   1570 
   1571    /// Check if bot is running
   1572    pub fn is_running(&self) -> bool {
   1573        *self.running.lock().unwrap()
   1574    }
   1575 }