account_management.rs (13156B)
1 use crate::Users; 2 use chrono::{DateTime, Utc}; 3 use serde::{Deserialize, Serialize}; 4 use std::collections::HashMap; 5 use std::sync::{Arc, Mutex}; 6 use std::cell::RefCell; 7 8 /// Represents the relationship status between master and alt accounts 9 #[derive(Debug, Clone, Serialize, Deserialize)] 10 pub enum AccountRelationshipStatus { 11 /// Both accounts are online and linked 12 Active, 13 /// Master account is offline (may be in incognito mode) 14 MasterOffline, 15 /// Alt account is offline 16 AltOffline, 17 /// Both accounts are offline 18 BothOffline, 19 /// No relationship configured 20 None, 21 } 22 23 /// Enhanced account management system 24 #[derive(Debug, Clone)] 25 pub struct AccountManager { 26 /// Current username 27 pub current_user: String, 28 /// Master account username (if this is an alt) 29 pub master_account: Option<String>, 30 /// Alt account username (if this is a master) 31 pub alt_account: Option<String>, 32 /// Whether this instance is running as a master account 33 pub is_master: bool, 34 /// Last time accounts were verified as online together 35 pub last_verified_together: RefCell<Option<DateTime<Utc>>>, 36 /// Custom commands that can be executed by alt on behalf of master 37 pub delegated_commands: HashMap<String, String>, 38 /// Whether incognito mode detection is enabled 39 pub incognito_detection_enabled: bool, 40 } 41 42 impl Default for AccountManager { 43 fn default() -> Self { 44 Self { 45 current_user: String::new(), 46 master_account: None, 47 alt_account: None, 48 is_master: false, 49 last_verified_together: RefCell::new(None), 50 delegated_commands: HashMap::new(), 51 incognito_detection_enabled: true, 52 } 53 } 54 } 55 56 impl AccountManager { 57 /// Create a new account manager 58 pub fn new(current_user: String) -> Self { 59 Self { 60 current_user, 61 ..Default::default() 62 } 63 } 64 65 /// Set master account relationship 66 pub fn set_master_account(&mut self, master: String) { 67 self.master_account = Some(master); 68 self.is_master = false; 69 self.setup_default_delegated_commands(); 70 } 71 72 /// Set alt account relationship 73 pub fn set_alt_account(&mut self, alt: String) { 74 self.alt_account = Some(alt); 75 self.is_master = true; 76 self.setup_default_delegated_commands(); 77 } 78 79 /// Check the current relationship status 80 pub fn get_relationship_status(&self, users: &Arc<Mutex<Users>>) -> AccountRelationshipStatus { 81 if self.master_account.is_none() && self.alt_account.is_none() { 82 return AccountRelationshipStatus::None; 83 } 84 85 let users = users.lock().unwrap(); 86 let all_users: Vec<String> = users.all().iter().map(|(_, name)| name.clone()).collect(); 87 88 match (&self.master_account, &self.alt_account) { 89 (Some(master), None) => { 90 // This is an alt account, check if master is online 91 if all_users.contains(master) { 92 self.update_last_verified(); 93 AccountRelationshipStatus::Active 94 } else if self.incognito_detection_enabled { 95 // Master might be in incognito mode, check recent activity 96 if self.was_recently_verified() { 97 AccountRelationshipStatus::Active 98 } else { 99 AccountRelationshipStatus::MasterOffline 100 } 101 } else { 102 AccountRelationshipStatus::MasterOffline 103 } 104 } 105 (None, Some(alt)) => { 106 // This is a master account, check if alt is online 107 if all_users.contains(alt) { 108 self.update_last_verified(); 109 AccountRelationshipStatus::Active 110 } else { 111 AccountRelationshipStatus::AltOffline 112 } 113 } 114 (Some(master), Some(_)) => { 115 // Both are configured (shouldn't happen but handle gracefully) 116 if all_users.contains(master) { 117 self.update_last_verified(); 118 AccountRelationshipStatus::Active 119 } else { 120 AccountRelationshipStatus::MasterOffline 121 } 122 } 123 (None, None) => AccountRelationshipStatus::None, 124 } 125 } 126 127 /// Check if accounts were recently verified together (for incognito mode detection) 128 fn was_recently_verified(&self) -> bool { 129 if let Some(last_verified) = *self.last_verified_together.borrow() { 130 let now = Utc::now(); 131 let duration = now.signed_duration_since(last_verified); 132 duration.num_minutes() < 10 // Consider active if verified within 10 minutes 133 } else { 134 false 135 } 136 } 137 138 /// Update the last verified timestamp 139 fn update_last_verified(&self) { 140 *self.last_verified_together.borrow_mut() = Some(Utc::now()); 141 } 142 143 /// Check if a command can be delegated from alt to master 144 pub fn can_delegate_command(&self, command: &str) -> bool { 145 if !self.is_relationship_active_cached() { 146 return false; 147 } 148 149 // Allow basic commands and custom aliases 150 self.delegated_commands.contains_key(command) || 151 self.is_safe_delegated_command(command) 152 } 153 154 /// Check if a command is safe for delegation without explicit configuration 155 fn is_safe_delegated_command(&self, command: &str) -> bool { 156 let safe_commands = [ 157 "pm", "kick", "k", "ban", "unban", "filter", "unfilter", 158 "dl", "dall", "ignore", "unignore", "op", "deop", 159 "voice", "devoice", "topic", "motd", "rules" 160 ]; 161 162 // Check if it's a basic safe command 163 if safe_commands.contains(&command) { 164 return true; 165 } 166 167 // Check if it's a custom command (starting with !) 168 if command.starts_with('!') { 169 return true; 170 } 171 172 // Check if it's an alias command (custom user command) 173 command.chars().all(|c| c.is_alphanumeric() || c == '_') 174 } 175 176 /// Execute a delegated command from alt to master 177 pub fn execute_delegated_command(&self, command: &str, args: &[&str]) -> Option<String> { 178 if !self.can_delegate_command(command) { 179 return None; 180 } 181 182 match command { 183 // Handle PM forwarding with enhanced context 184 "pm" => { 185 if args.len() >= 2 { 186 let target = args[0]; 187 let message = args[1..].join(" "); 188 if self.master_account.is_some() { 189 Some(format!("/pm {} [via {}] {}", target, self.current_user, message)) 190 } else { 191 None 192 } 193 } else { 194 None 195 } 196 } 197 198 // Handle moderation commands 199 "kick" | "k" => { 200 if !args.is_empty() { 201 let target = args[0]; 202 let reason = if args.len() > 1 { 203 format!(" {}", args[1..].join(" ")) 204 } else { 205 String::new() 206 }; 207 if let Some(master) = &self.master_account { 208 Some(format!("/pm {} #kick {}{} (requested by {})", master, target, reason, self.current_user)) 209 } else { 210 Some(format!("/{} {}{}", command, target, reason)) 211 } 212 } else { 213 None 214 } 215 } 216 217 "ban" => { 218 if !args.is_empty() { 219 let target = args[0]; 220 let reason = if args.len() > 1 { 221 format!(" {}", args[1..].join(" ")) 222 } else { 223 String::new() 224 }; 225 if let Some(master) = &self.master_account { 226 Some(format!("/pm {} #ban {}{} (requested by {})", master, target, reason, self.current_user)) 227 } else { 228 Some(format!("/ban {}{}", target, reason)) 229 } 230 } else { 231 None 232 } 233 } 234 235 // Handle custom delegated commands 236 _ => { 237 if let Some(template) = self.delegated_commands.get(command) { 238 let mut result = template.clone(); 239 // Replace placeholders with arguments 240 for (i, arg) in args.iter().enumerate() { 241 result = result.replace(&format!("{{{}}}", i), arg); 242 } 243 Some(result) 244 } else if command.starts_with('!') { 245 // Custom user command - execute directly 246 Some(command.to_string()) 247 } else { 248 // Try to execute as direct command 249 let full_command = if args.is_empty() { 250 format!("/{}", command) 251 } else { 252 format!("/{} {}", command, args.join(" ")) 253 }; 254 Some(full_command) 255 } 256 } 257 } 258 } 259 260 /// Set up default delegated commands 261 fn setup_default_delegated_commands(&mut self) { 262 self.delegated_commands.clear(); 263 264 // Add some useful default templates 265 self.delegated_commands.insert( 266 "warn".to_string(), 267 "/pm {0} This is your warning @{0}, will be kicked next !rules".to_string() 268 ); 269 270 self.delegated_commands.insert( 271 "welcome".to_string(), 272 "Welcome to the chat @{0}! Please read the !rules".to_string() 273 ); 274 275 self.delegated_commands.insert( 276 "op".to_string(), 277 "/op {0}".to_string() 278 ); 279 280 self.delegated_commands.insert( 281 "deop".to_string(), 282 "/deop {0}".to_string() 283 ); 284 } 285 286 /// Get the related account name 287 pub fn get_related_account(&self) -> Option<&String> { 288 self.master_account.as_ref().or(self.alt_account.as_ref()) 289 } 290 291 /// Check if relationship is currently active (cached version for const contexts) 292 fn is_relationship_active_cached(&self) -> bool { 293 // This is a simplified check - in practice, you'd want to cache the last status 294 // For now, assume active if relationship exists and was recently verified 295 self.get_related_account().is_some() && self.was_recently_verified() 296 } 297 298 /// Format a status message for display 299 pub fn format_status_message(&self, status: &AccountRelationshipStatus) -> String { 300 match status { 301 AccountRelationshipStatus::Active => { 302 if let Some(related) = self.get_related_account() { 303 if self.is_master { 304 format!("🔗 Master account linked to alt: {} (Active)", related) 305 } else { 306 format!("🔗 Alt account linked to master: {} (Active)", related) 307 } 308 } else { 309 "🔗 Account relationship active".to_string() 310 } 311 } 312 AccountRelationshipStatus::MasterOffline => { 313 if let Some(master) = &self.master_account { 314 format!("⚠️ Master account {} appears offline (may be incognito)", master) 315 } else { 316 "⚠️ Master account offline".to_string() 317 } 318 } 319 AccountRelationshipStatus::AltOffline => { 320 if let Some(alt) = &self.alt_account { 321 format!("⚠️ Alt account {} is offline", alt) 322 } else { 323 "⚠️ Alt account offline".to_string() 324 } 325 } 326 AccountRelationshipStatus::BothOffline => { 327 "❌ Both master and alt accounts are offline".to_string() 328 } 329 AccountRelationshipStatus::None => { 330 "No master/alt relationship configured".to_string() 331 } 332 } 333 } 334 } 335 336 /// Enhanced command parsing that handles master/alt delegation 337 pub fn parse_enhanced_command( 338 input: &str, 339 account_manager: &AccountManager 340 ) -> Option<String> { 341 if input.starts_with('/') { 342 let parts: Vec<&str> = input[1..].split_whitespace().collect(); 343 if !parts.is_empty() { 344 let command = parts[0]; 345 let args: Vec<&str> = parts[1..].iter().cloned().collect(); 346 347 // Check if this command can be delegated 348 if account_manager.can_delegate_command(command) { 349 return account_manager.execute_delegated_command(command, &args); 350 } 351 } 352 } 353 354 // Return original input if no delegation needed 355 Some(input.to_string()) 356 }