bhcli

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

bot_client.rs (8579B)


      1 use crate::ai_service::AIService;
      2 use crate::bot_system::{BotConfig, BotSystem, MessageType};
      3 use crate::PostType;
      4 use anyhow::{anyhow, Result};
      5 use crossbeam_channel::Sender;
      6 use log::{error, info, warn};
      7 use std::sync::{Arc, Mutex};
      8 use std::thread;
      9 use std::time::Duration;
     10 use tokio::runtime::Runtime;
     11 
     12 /// Bot client that runs in the background
     13 pub struct BotClient {
     14    bot_name: String,
     15    bot_system: Arc<BotSystem>,
     16    running: Arc<Mutex<bool>>,
     17    tx: Sender<PostType>,
     18    rx: Arc<Mutex<crossbeam_channel::Receiver<PostType>>>,
     19 }
     20 
     21 impl BotClient {
     22    /// Create a new bot client
     23    pub fn new(
     24        bot_name: String,
     25        _username: String,
     26        _password: String,
     27        _url: String,
     28        ai_service: Option<Arc<AIService>>,
     29        runtime: Option<Arc<Runtime>>,
     30        admin_users: Vec<String>,
     31    ) -> Result<Self> {
     32        // Create communication channels
     33        let (tx, rx) = crossbeam_channel::unbounded();
     34 
     35        // Create bot configuration
     36        let bot_config = BotConfig {
     37            bot_name: bot_name.clone(),
     38            admin_users,
     39            data_directory: std::env::current_dir()?.join("bot_data").join(&bot_name),
     40            ..BotConfig::default()
     41        };
     42 
     43        // Create bot system
     44        let bot_system = Arc::new(BotSystem::new(bot_config, tx.clone(), ai_service, runtime)?);
     45 
     46        Ok(Self {
     47            bot_name,
     48            bot_system,
     49            running: Arc::new(Mutex::new(false)),
     50            tx,
     51            rx: Arc::new(Mutex::new(rx)),
     52        })
     53    }
     54 
     55    /// Start the bot client
     56    pub fn start(&mut self) -> Result<()> {
     57        {
     58            let mut running = self.running.lock().unwrap();
     59            if *running {
     60                return Err(anyhow!("Bot client is already running"));
     61            }
     62            *running = true;
     63        }
     64 
     65        info!("Starting bot client: {}", self.bot_name);
     66 
     67        // Start bot system
     68        self.bot_system.start()?;
     69 
     70        // Since we can't easily create a separate LeChatPHPClient instance,
     71        // we'll simulate the bot by creating a background thread that processes
     72        // messages and responds to commands.
     73        self.start_message_processing_thread()?;
     74 
     75        Ok(())
     76    }
     77 
     78    /// Stop the bot client
     79    pub fn stop(&mut self) -> Result<()> {
     80        {
     81            let mut running = self.running.lock().unwrap();
     82            if !*running {
     83                return Ok(());
     84            }
     85            *running = false;
     86        }
     87 
     88        info!("Stopping bot client: {}", self.bot_name);
     89 
     90        // Stop bot system
     91        self.bot_system.stop()?;
     92 
     93        Ok(())
     94    }
     95 
     96    /// Start message processing thread
     97    fn start_message_processing_thread(&self) -> Result<()> {
     98        let bot_system = Arc::clone(&self.bot_system);
     99        let running = Arc::clone(&self.running);
    100        let bot_name = self.bot_name.clone();
    101        let tx = self.tx.clone();
    102 
    103        thread::spawn(move || {
    104            info!("Bot {} message processing thread started", bot_name);
    105 
    106            // Send initial status message (non-blocking)
    107            let startup_msg = format!("🤖 {} is now online and ready to help!", bot_name);
    108            if let Err(e) = tx.try_send(PostType::Post(startup_msg, Some("0".to_string()))) {
    109                warn!(
    110                    "Could not send bot startup message (channel may be disconnected): {}",
    111                    e
    112                );
    113            }
    114 
    115            while *running.lock().unwrap() {
    116                // In a real implementation, this would:
    117                // 1. Monitor main chat messages for @bot mentions
    118                // 2. Process the messages through the bot system
    119                // 3. Send appropriate responses
    120 
    121                // For now, we'll just maintain the bot system state
    122                thread::sleep(Duration::from_secs(30));
    123 
    124                // Send periodic status if needed (for debugging)
    125                if bot_system.is_running() {
    126                    // Bot is active and ready to respond to commands
    127                    // Commands will be processed when main client detects @bot mentions
    128                }
    129            }
    130 
    131            // Send shutdown message (non-blocking)
    132            let shutdown_msg = format!("🤖 {} is going offline. Data has been saved.", bot_name);
    133            if let Err(e) = tx.try_send(PostType::Post(shutdown_msg, Some("0".to_string()))) {
    134                warn!(
    135                    "Could not send bot shutdown message (channel may be disconnected): {}",
    136                    e
    137                );
    138            }
    139 
    140            info!("Bot {} message processing thread stopped", bot_name);
    141        });
    142 
    143        Ok(())
    144    }
    145 
    146    /// Process a message for the bot system
    147    #[allow(dead_code)]
    148    pub fn process_message(
    149        &self,
    150        username: &str,
    151        content: &str,
    152        message_id: Option<usize>,
    153        is_private: bool,
    154    ) -> Result<()> {
    155        // Determine message type
    156        let message_type = if is_private {
    157            MessageType::PrivateMessage {
    158                to: self.bot_name.clone(),
    159            }
    160        } else {
    161            MessageType::Normal
    162        };
    163 
    164        // Process through bot system
    165        self.bot_system.process_message(
    166            username,
    167            content,
    168            message_type,
    169            message_id.map(|id| id as u64),
    170            None,  // No channel context for individual bot calls
    171            false, // Assume non-member for individual calls
    172        )?;
    173 
    174        Ok(())
    175    }
    176 
    177    /// Check if bot is running
    178    pub fn is_running(&self) -> bool {
    179        *self.running.lock().unwrap()
    180    }
    181 
    182    /// Get bot name
    183    pub fn get_bot_name(&self) -> &str {
    184        &self.bot_name
    185    }
    186 }
    187 
    188 impl Drop for BotClient {
    189    fn drop(&mut self) {
    190        if self.is_running() {
    191            if let Err(e) = self.stop() {
    192                error!("Failed to stop bot client during drop: {}", e);
    193            }
    194        }
    195    }
    196 }
    197 
    198 /// Bot manager to handle multiple bots
    199 pub struct BotManager {
    200    bots: Vec<BotClient>,
    201    ai_service: Option<Arc<AIService>>,
    202    runtime: Option<Arc<Runtime>>,
    203 }
    204 
    205 impl BotManager {
    206    pub fn new(ai_service: Option<Arc<AIService>>, runtime: Option<Arc<Runtime>>) -> Self {
    207        Self {
    208            bots: Vec::new(),
    209            ai_service,
    210            runtime,
    211        }
    212    }
    213 
    214    pub fn add_bot(
    215        &mut self,
    216        bot_name: String,
    217        username: String,
    218        password: String,
    219        url: String,
    220        admin_users: Vec<String>,
    221    ) -> Result<()> {
    222        let bot = BotClient::new(
    223            bot_name,
    224            username,
    225            password,
    226            url,
    227            self.ai_service.clone(),
    228            self.runtime.clone(),
    229            admin_users,
    230        )?;
    231 
    232        self.bots.push(bot);
    233        Ok(())
    234    }
    235 
    236    /// Get all bot receivers for message forwarding
    237    pub fn get_all_bot_receivers(
    238        &self,
    239    ) -> Vec<(String, Arc<Mutex<crossbeam_channel::Receiver<PostType>>>)> {
    240        self.bots
    241            .iter()
    242            .map(|bot| (bot.get_bot_name().to_string(), Arc::clone(&bot.rx)))
    243            .collect()
    244    }
    245 
    246    pub fn start_bot(&mut self, bot_name: &str) -> Result<()> {
    247        if let Some(bot) = self.bots.iter_mut().find(|b| b.get_bot_name() == bot_name) {
    248            bot.start()?;
    249            info!("Started bot: {}", bot_name);
    250        } else {
    251            return Err(anyhow!("Bot not found: {}", bot_name));
    252        }
    253        Ok(())
    254    }
    255 
    256    /// Stop all bots
    257    pub fn stop_all(&mut self) -> Result<()> {
    258        for bot in &mut self.bots {
    259            if bot.is_running() {
    260                bot.stop()?;
    261            }
    262        }
    263        Ok(())
    264    }
    265 
    266    /// Process message for all bots
    267    pub fn process_message_for_all_bots(
    268        &self,
    269        username: &str,
    270        content: &str,
    271        message_type: MessageType,
    272        message_id: Option<u64>,
    273        channel_context: Option<&str>,
    274        is_member: bool,
    275    ) -> Result<()> {
    276        for bot in &self.bots {
    277            if bot.is_running() {
    278                if let Err(e) = bot.bot_system.process_message(
    279                    username,
    280                    content,
    281                    message_type.clone(),
    282                    message_id,
    283                    channel_context,
    284                    is_member,
    285                ) {
    286                    warn!(
    287                        "Failed to process message for bot {}: {}",
    288                        bot.get_bot_name(),
    289                        e
    290                    );
    291                }
    292            }
    293        }
    294        Ok(())
    295    }
    296 }