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(×tamp_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 }