bhcli

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

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 }