bhcli

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

tools.rs (16171B)


      1 use crate::chatops::{ChatCommand, ChatOpError, ChatOpResult, CommandContext};
      2 use base64::{engine::general_purpose, Engine as _};
      3 use chrono::{DateTime, Utc};
      4 use rand::Rng;
      5 use regex::Regex;
      6 use std::process::Command;
      7 use std::time::{SystemTime, UNIX_EPOCH};
      8 use uuid::Uuid;
      9 
     10 /// Hash generation command
     11 pub struct HashCommand;
     12 
     13 impl ChatCommand for HashCommand {
     14    fn name(&self) -> &'static str {
     15        "hash"
     16    }
     17    fn description(&self) -> &'static str {
     18        "Generate hash of text using various algorithms"
     19    }
     20    fn usage(&self) -> &'static str {
     21        "/hash <algorithm> <text>"
     22    }
     23 
     24    fn execute(
     25        &self,
     26        args: Vec<String>,
     27        _context: &CommandContext,
     28    ) -> Result<ChatOpResult, ChatOpError> {
     29        if args.len() < 2 {
     30            return Err(ChatOpError::MissingArguments(
     31                "Please specify algorithm and text".to_string(),
     32            ));
     33        }
     34 
     35        let algorithm = args[0].to_lowercase();
     36        let text = args[1..].join(" ");
     37 
     38        let hash_result = match algorithm.as_str() {
     39            "md5" => {
     40                let digest = md5::compute(text.as_bytes());
     41                format!("{:x}", digest)
     42            }
     43            "sha1" => {
     44                use sha1::{Digest, Sha1};
     45                let mut hasher = Sha1::new();
     46                hasher.update(text.as_bytes());
     47                format!("{:x}", hasher.finalize())
     48            }
     49            "sha256" => {
     50                use sha2::{Digest, Sha256};
     51                let mut hasher = Sha256::new();
     52                hasher.update(text.as_bytes());
     53                format!("{:x}", hasher.finalize())
     54            }
     55            "sha512" => {
     56                use sha2::{Digest, Sha512};
     57                let mut hasher = Sha512::new();
     58                hasher.update(text.as_bytes());
     59                format!("{:x}", hasher.finalize())
     60            }
     61            _ => {
     62                return Err(ChatOpError::InvalidSyntax(
     63                    "Supported algorithms: md5, sha1, sha256, sha512".to_string(),
     64                ))
     65            }
     66        };
     67 
     68        Ok(ChatOpResult::Message(format!(
     69            "🔐 {} hash: `{}`",
     70            algorithm.to_uppercase(),
     71            hash_result
     72        )))
     73    }
     74 }
     75 
     76 /// UUID generation command
     77 pub struct UuidCommand;
     78 
     79 impl ChatCommand for UuidCommand {
     80    fn name(&self) -> &'static str {
     81        "uuid"
     82    }
     83    fn description(&self) -> &'static str {
     84        "Generate a random UUID"
     85    }
     86    fn usage(&self) -> &'static str {
     87        "/uuid"
     88    }
     89 
     90    fn execute(
     91        &self,
     92        _args: Vec<String>,
     93        _context: &CommandContext,
     94    ) -> Result<ChatOpResult, ChatOpError> {
     95        let uuid = Uuid::new_v4();
     96        Ok(ChatOpResult::Message(format!(
     97            "🆔 Generated UUID: `{}`",
     98            uuid
     99        )))
    100    }
    101 }
    102 
    103 /// Base64 encoding/decoding command
    104 pub struct Base64Command;
    105 
    106 impl ChatCommand for Base64Command {
    107    fn name(&self) -> &'static str {
    108        "base64"
    109    }
    110    fn description(&self) -> &'static str {
    111        "Encode or decode Base64 text"
    112    }
    113    fn usage(&self) -> &'static str {
    114        "/base64 <encode|decode> <text>"
    115    }
    116 
    117    fn execute(
    118        &self,
    119        args: Vec<String>,
    120        _context: &CommandContext,
    121    ) -> Result<ChatOpResult, ChatOpError> {
    122        if args.len() < 2 {
    123            return Err(ChatOpError::MissingArguments(
    124                "Please specify operation (encode/decode) and text".to_string(),
    125            ));
    126        }
    127 
    128        let operation = args[0].to_lowercase();
    129        let text = args[1..].join(" ");
    130 
    131        match operation.as_str() {
    132            "encode" | "enc" => {
    133                let encoded = general_purpose::STANDARD.encode(text.as_bytes());
    134                Ok(ChatOpResult::Message(format!(
    135                    "🔤 Base64 encoded: `{}`",
    136                    encoded
    137                )))
    138            }
    139            "decode" | "dec" => match general_purpose::STANDARD.decode(&text) {
    140                Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
    141                    Ok(decoded_text) => Ok(ChatOpResult::Message(format!(
    142                        "🔤 Base64 decoded: `{}`",
    143                        decoded_text
    144                    ))),
    145                    Err(_) => Ok(ChatOpResult::Message(
    146                        "🔤 Base64 decoded to binary data (not valid UTF-8)".to_string(),
    147                    )),
    148                },
    149                Err(_) => Err(ChatOpError::InvalidSyntax(
    150                    "Invalid Base64 input".to_string(),
    151                )),
    152            },
    153            _ => Err(ChatOpError::InvalidSyntax(
    154                "Operation must be 'encode' or 'decode'".to_string(),
    155            )),
    156        }
    157    }
    158 }
    159 
    160 /// Regex testing command
    161 pub struct RegexCommand;
    162 
    163 impl ChatCommand for RegexCommand {
    164    fn name(&self) -> &'static str {
    165        "regex"
    166    }
    167    fn description(&self) -> &'static str {
    168        "Test regex pattern against text"
    169    }
    170    fn usage(&self) -> &'static str {
    171        "/regex <pattern> <text>"
    172    }
    173    fn aliases(&self) -> Vec<&'static str> {
    174        vec!["re"]
    175    }
    176 
    177    fn execute(
    178        &self,
    179        args: Vec<String>,
    180        _context: &CommandContext,
    181    ) -> Result<ChatOpResult, ChatOpError> {
    182        if args.len() < 2 {
    183            return Err(ChatOpError::MissingArguments(
    184                "Please specify pattern and text".to_string(),
    185            ));
    186        }
    187 
    188        let pattern = &args[0];
    189        let text = args[1..].join(" ");
    190 
    191        match Regex::new(pattern) {
    192            Ok(re) => {
    193                let matches: Vec<_> = re.find_iter(&text).collect();
    194 
    195                if matches.is_empty() {
    196                    Ok(ChatOpResult::Message("🔍 No matches found".to_string()))
    197                } else {
    198                    let mut result = vec![format!("🔍 Found {} match(es):", matches.len())];
    199 
    200                    for (i, m) in matches.iter().enumerate().take(5) {
    201                        // Limit to 5 matches
    202                        result.push(format!(
    203                            "  {}. \"{}\" at position {}-{}",
    204                            i + 1,
    205                            m.as_str(),
    206                            m.start(),
    207                            m.end()
    208                        ));
    209                    }
    210 
    211                    if matches.len() > 5 {
    212                        result.push(format!("  ... and {} more", matches.len() - 5));
    213                    }
    214 
    215                    Ok(ChatOpResult::Block(result))
    216                }
    217            }
    218            Err(e) => Err(ChatOpError::InvalidSyntax(format!(
    219                "Invalid regex pattern: {}",
    220                e
    221            ))),
    222        }
    223    }
    224 }
    225 
    226 /// WHOIS lookup command
    227 pub struct WhoisCommand;
    228 
    229 impl ChatCommand for WhoisCommand {
    230    fn name(&self) -> &'static str {
    231        "whois"
    232    }
    233    fn description(&self) -> &'static str {
    234        "Perform WHOIS lookup on a domain"
    235    }
    236    fn usage(&self) -> &'static str {
    237        "/whois <domain>"
    238    }
    239 
    240    fn execute(
    241        &self,
    242        args: Vec<String>,
    243        _context: &CommandContext,
    244    ) -> Result<ChatOpResult, ChatOpError> {
    245        if args.is_empty() {
    246            return Err(ChatOpError::MissingArguments(
    247                "Please specify a domain".to_string(),
    248            ));
    249        }
    250 
    251        let domain = &args[0];
    252 
    253        match Command::new("whois").arg(domain).output() {
    254            Ok(output) => {
    255                if output.status.success() {
    256                    let result = String::from_utf8_lossy(&output.stdout);
    257                    let lines: Vec<String> = result
    258                        .lines()
    259                        .filter(|line| !line.trim().is_empty() && !line.starts_with('%'))
    260                        .take(10) // Limit output
    261                        .map(|line| line.trim().to_string())
    262                        .collect();
    263 
    264                    if lines.is_empty() {
    265                        Ok(ChatOpResult::Message(format!(
    266                            "🌐 No WHOIS data found for '{}'",
    267                            domain
    268                        )))
    269                    } else {
    270                        Ok(ChatOpResult::Block(lines))
    271                    }
    272                } else {
    273                    Ok(ChatOpResult::Message(format!(
    274                        "🌐 WHOIS lookup failed for '{}'",
    275                        domain
    276                    )))
    277                }
    278            }
    279            Err(_) => Ok(ChatOpResult::Message(format!(
    280                "🌐 WHOIS command not available. Try: https://whois.net/whois/{}",
    281                domain
    282            ))),
    283        }
    284    }
    285 }
    286 
    287 /// DNS lookup command
    288 pub struct DigCommand;
    289 
    290 impl ChatCommand for DigCommand {
    291    fn name(&self) -> &'static str {
    292        "dig"
    293    }
    294    fn description(&self) -> &'static str {
    295        "Perform DNS lookup"
    296    }
    297    fn usage(&self) -> &'static str {
    298        "/dig <domain> [record_type]"
    299    }
    300 
    301    fn execute(
    302        &self,
    303        args: Vec<String>,
    304        _context: &CommandContext,
    305    ) -> Result<ChatOpResult, ChatOpError> {
    306        if args.is_empty() {
    307            return Err(ChatOpError::MissingArguments(
    308                "Please specify a domain".to_string(),
    309            ));
    310        }
    311 
    312        let domain = &args[0];
    313        let record_type = args.get(1).map(|s| s.as_str()).unwrap_or("A");
    314 
    315        match Command::new("dig")
    316            .args(&["+short", domain, record_type])
    317            .output()
    318        {
    319            Ok(output) => {
    320                if output.status.success() {
    321                    let result = String::from_utf8_lossy(&output.stdout);
    322                    let lines: Vec<String> = result
    323                        .lines()
    324                        .filter(|line| !line.trim().is_empty())
    325                        .map(|line| format!("🌐 {} {} {}", domain, record_type, line.trim()))
    326                        .collect();
    327 
    328                    if lines.is_empty() {
    329                        Ok(ChatOpResult::Message(format!("🌐 No {} records found for '{}'", record_type, domain)))
    330                    } else {
    331                        Ok(ChatOpResult::Block(lines))
    332                    }
    333                } else {
    334                    Ok(ChatOpResult::Message(format!("🌐 DNS lookup failed for '{}'", domain)))
    335                }
    336            }
    337            Err(_) => Ok(ChatOpResult::Message(format!("🌐 dig command not available. Try: https://www.nslookup.io/domains/{}/dns-records/", domain))),
    338        }
    339    }
    340 }
    341 
    342 /// IP information lookup command
    343 pub struct IpInfoCommand;
    344 
    345 impl ChatCommand for IpInfoCommand {
    346    fn name(&self) -> &'static str {
    347        "ipinfo"
    348    }
    349    fn description(&self) -> &'static str {
    350        "Get IP address information"
    351    }
    352    fn usage(&self) -> &'static str {
    353        "/ipinfo <ip_address>"
    354    }
    355    fn aliases(&self) -> Vec<&'static str> {
    356        vec!["ip"]
    357    }
    358 
    359    fn execute(
    360        &self,
    361        args: Vec<String>,
    362        _context: &CommandContext,
    363    ) -> Result<ChatOpResult, ChatOpError> {
    364        if args.is_empty() {
    365            return Err(ChatOpError::MissingArguments(
    366                "Please specify an IP address".to_string(),
    367            ));
    368        }
    369 
    370        let ip = &args[0];
    371 
    372        // Basic IP validation
    373        if !ip.chars().all(|c| c.is_ascii_digit() || c == '.') || ip.split('.').count() != 4 {
    374            return Err(ChatOpError::InvalidSyntax(
    375                "Please provide a valid IPv4 address".to_string(),
    376            ));
    377        }
    378 
    379        // Try to get info from a free service
    380        match Command::new("curl")
    381            .args(&["-s", &format!("https://ipinfo.io/{}/json", ip)])
    382            .output()
    383        {
    384            Ok(output) => {
    385                if output.status.success() {
    386                    let result = String::from_utf8_lossy(&output.stdout);
    387                    // Simple parsing - in a real implementation you'd use serde_json
    388                    if result.contains("\"ip\"") {
    389                        Ok(ChatOpResult::CodeBlock(
    390                            result.to_string(),
    391                            Some("json".to_string()),
    392                        ))
    393                    } else {
    394                        Ok(ChatOpResult::Message(format!(
    395                            "🌍 No information found for IP: {}",
    396                            ip
    397                        )))
    398                    }
    399                } else {
    400                    Ok(ChatOpResult::Message(format!(
    401                        "🌍 IP lookup failed for '{}'",
    402                        ip
    403                    )))
    404                }
    405            }
    406            Err(_) => Ok(ChatOpResult::Message(format!(
    407                "🌍 IP lookup not available. Try: https://ipinfo.io/{}",
    408                ip
    409            ))),
    410        }
    411    }
    412 }
    413 
    414 /// Random number generator command
    415 pub struct RandCommand;
    416 
    417 impl ChatCommand for RandCommand {
    418    fn name(&self) -> &'static str {
    419        "rand"
    420    }
    421    fn description(&self) -> &'static str {
    422        "Generate random number"
    423    }
    424    fn usage(&self) -> &'static str {
    425        "/rand [min] [max]"
    426    }
    427    fn aliases(&self) -> Vec<&'static str> {
    428        vec!["random", "rng"]
    429    }
    430 
    431    fn execute(
    432        &self,
    433        args: Vec<String>,
    434        _context: &CommandContext,
    435    ) -> Result<ChatOpResult, ChatOpError> {
    436        let mut rng = rand::thread_rng();
    437 
    438        match args.len() {
    439            0 => {
    440                // Random float between 0 and 1
    441                let num: f64 = rng.gen();
    442                Ok(ChatOpResult::Message(format!("🎲 Random: {:.6}", num)))
    443            }
    444            1 => {
    445                // Random int between 0 and max
    446                match args[0].parse::<i32>() {
    447                    Ok(max) if max > 0 => {
    448                        let num = rng.gen_range(0..=max);
    449                        Ok(ChatOpResult::Message(format!(
    450                            "🎲 Random (0-{}): {}",
    451                            max, num
    452                        )))
    453                    }
    454                    _ => Err(ChatOpError::InvalidSyntax(
    455                        "Max must be a positive integer".to_string(),
    456                    )),
    457                }
    458            }
    459            2 => {
    460                // Random int between min and max
    461                match (args[0].parse::<i32>(), args[1].parse::<i32>()) {
    462                    (Ok(min), Ok(max)) if min < max => {
    463                        let num = rng.gen_range(min..=max);
    464                        Ok(ChatOpResult::Message(format!(
    465                            "🎲 Random ({}-{}): {}",
    466                            min, max, num
    467                        )))
    468                    }
    469                    _ => Err(ChatOpError::InvalidSyntax(
    470                        "Min and max must be integers with min < max".to_string(),
    471                    )),
    472                }
    473            }
    474            _ => Err(ChatOpError::InvalidSyntax("Too many arguments".to_string())),
    475        }
    476    }
    477 }
    478 
    479 /// Time display command
    480 pub struct TimeCommand;
    481 
    482 impl ChatCommand for TimeCommand {
    483    fn name(&self) -> &'static str {
    484        "time"
    485    }
    486    fn description(&self) -> &'static str {
    487        "Show current time in UTC and local"
    488    }
    489    fn usage(&self) -> &'static str {
    490        "/time"
    491    }
    492 
    493    fn execute(
    494        &self,
    495        _args: Vec<String>,
    496        _context: &CommandContext,
    497    ) -> Result<ChatOpResult, ChatOpError> {
    498        let now = SystemTime::now();
    499        let utc: DateTime<Utc> = now.into();
    500 
    501        // Get local time zone if possible
    502        let local_info = match Command::new("date").output() {
    503            Ok(output) => String::from_utf8_lossy(&output.stdout).trim().to_string(),
    504            Err(_) => "Local time unavailable".to_string(),
    505        };
    506 
    507        let unix_timestamp = now
    508            .duration_since(UNIX_EPOCH)
    509            .map(|d| d.as_secs())
    510            .unwrap_or(0);
    511 
    512        let result = vec![
    513            "🕐 **Current Time:**".to_string(),
    514            format!("UTC: {}", utc.format("%Y-%m-%d %H:%M:%S UTC")),
    515            format!("Local: {}", local_info),
    516            format!("Unix Timestamp: {}", unix_timestamp),
    517        ];
    518 
    519        Ok(ChatOpResult::Block(result))
    520    }
    521 }